天天看點

百度品質部電話面試題

1:兩個連結清單找公共節點:

struct ListNode 
{ 
      int     m_nKey; 
      ListNode*   m_pNext; 
}; 
           

分析:這是一道微軟的面試題。微軟非常喜歡與連結清單相關的題目,是以在微軟的

面試題中,連結清單出現的機率相當高。 

如果兩個單向連結清單有公共的結點,也就是說兩個連結清單從某一結點開始,它們的

m_pNext都指向同一個結點。但由于是單向連結清單的結點,每個結點隻有一個

m_pNext,是以從第一個公共結點開始,之後它們所有結點都是重合的,不可能

再出現分叉。是以,兩個有公共結點而部分重合的連結清單,拓撲形狀看起來像一個

Y,而不可能像 X。 

看到這個題目,第一反應就是蠻力法:在第一連結清單上順序周遊每個結點。每周遊

一個結點的時候,在第二個連結清單上順序周遊每個結點。如果此時兩個連結清單上的結

點是一樣的,說明此時兩個連結清單重合,于是找到了它們的公共結點。如果第一個

連結清單的長度為 m,第二個連結清單的長度為 n,顯然,該方法的時間複雜度為 O(mn)。  

接下來我們試着去尋找一個線性時間複雜度的算法。我們先把問題簡化:如何判

斷兩個單向連結清單有沒有公共結點?前面已經提到,如果兩個連結清單有一個公共結

點,那麼該公共結點之後的所有結點都是重合的。那麼,它們的最後一個結點必

然是重合的。是以,我們判斷兩個連結清單是不是有重合的部分,隻要分别周遊兩個

連結清單到最後一個結點。如果兩個尾結點是一樣的,說明它們用重合;否則兩個鍊

表沒有公共的結點。 

在上面的思路中,順序周遊兩個連結清單到尾結點的時候,我們不能保證在兩個連結清單

上同時到達尾結點。這是因為兩個連結清單不一定長度一樣。但如果假設一個連結清單比

另一個長l個結點,我們先在長的連結清單上周遊 l個結點,之後再同步周遊,這個

時候我們就能保證同時到達最後一個結點了。由于兩個連結清單從第一個公共結點考

試到連結清單的尾結點,這一部分是重合的。是以,它們肯定也是同時到達第一公共

結點的。于是在周遊中,第一個相同的結點就是第一個公共的結點。 

在這個思路中,我們先要分别周遊兩個連結清單得到它們的長度,并求出兩個長度之

差。在長的連結清單上先周遊若幹次之後,再同步周遊兩個連結清單,知道找到相同的結

點,或者一直到連結清單結束。此時,如果第一個連結清單的長度為 m,第二個連結清單的長

度為n,該方法的時間複雜度為 O(m+n)。

int length(node *list){ //求長度
int len=0;
while(list){
len++;
list=list->next;
}
return len;
}
node *find_node(node *&listA,node *&listB){
int lenA=length(listA),lenB=length(listB);
node *lA=listA,*lB=listB;
if(lenA>lenB){
for(int i=1;i<=lenA-lenB;i++)
lA=lA->next;
}else{
for(int i=1;i<=lenB-lenA;i++)
lB=lB->next;
}
while(lA&&lB){
if(lA==lB){
return lA;
}else{
lA=lA->next;
lB=lB->next;
}
}
return NULL;
}
           

2:連結清單裡有一個循環,求此點

有一個單連結清單,其中可能有一個環,也就是某個節點的next指向的是連結清單中在它之前的節點,這樣在連結清單的尾部形成一環。

問題:

1、如何判斷一個連結清單是不是這類連結清單?

2、如果連結清單為存在環,如果找到環的入口點?

解答:

1、最簡單的方法, 用一個指針周遊連結清單, 每遇到一個節點就把他的記憶體位址(java中可以用object.hashcode())做為key放在一個hashtable中. 這樣當hashtable中出現重複key的時候說明此連結清單上有環. 這個方法的時間複雜度為O(n), 空間同樣為O(n). 

2、使用反轉指針的方法, 每過一個節點就把該節點的指針反向

Boolean reverse(Node *head) {

   Node *curr = head;

   Node *next = head->next;

   curr->next = NULL;

   while(next!=NULL) {

    if(next == head) { /* go back to the head of the list, so there is a loop */

      next->next = curr;

      return TRUE;

    }

    Node *temp = curr;

    curr = next;

    next = next->next;

    curr->next = temp;

   }

   /* at the end of list, so there is no loop, let's reverse the list back */

   next = curr->next;

   curr ->next = NULL;

   while(next!=NULL) {

    Node *temp = curr;

    curr = next;

    next = next->next;

    curr->next = temp;

   }

   return FALSE;

  }
           

看上去這是一種奇怪的方法: 當有環的時候反轉next指針會最終走到連結清單頭部; 當沒有環的時候反轉next指針會破壞連結清單結構(使連結清單反向), 是以需要最後把連結清單再反向一次. 這種方法的空間複雜度是O(1), 實事上我們使用了3個額外指針;而時間複雜度是O(n), 我們最多2次周遊整個連結清單(當連結清單中沒有環的時候).

這個方法的最大缺點是在多線程情況下不安全, 當多個線程都在讀這個連結清單的時候, 檢查環的線程會改變連結清單的狀态, 雖然最後我們恢複了連結清單本身的結構, 但是不能保證其他線程能得到正确的結果.

3、 這是一般面試官所預期的答案: 快指針和慢指針

設定兩個指針(fast, slow),初始值都指向頭,slow每次前進一步,fast每次前進二步,如果連結清單存在環,則fast必定先進入環,而slow後進入環,兩個指針必定相遇。(當然,fast先行頭到尾部為NULL,則為無環連結清單)

int   is_link_list_cicled(Node*   head)    
    {    
            Node   *p   =   head,   *q   =   head;    
            while(p   &&   q)    
            {                        
                    p   =   p-> next;    
                    q   =   q-> next;    
                    if(!q)    
                            return   0;    
                    q   =   q-> next;    
<pre name="code" class="cpp">                    if(p   ==   q)    
                            return   1;    
 } return 0; }  
           

證明步長法的正确性(追擊問題,如果有環則肯定可以相遇):

如果連結清單有環,不妨假設其環長度為M(>=2)。p指針首次到達交點(N0)時,q指針已經進入環。設p=0;q=q-p;再進過i(i>=0)步後,p=(p+i)%m;q=(q+2*i)%m;則必存在一個i使得(p+i)%m = (q+2*i)%m。(p,q 都為常數)。

二、找到環的入口點

當fast若與slow相遇時,slow肯定沒有走周遊完連結清單,而fast已經在環内循環了n圈(1<=n)。假設slow走了s步,則fast走了2s步(fast步數還等于s 加上在環上多轉的n圈),設環長為r,則:2s = s + nrs= nr設整個連結清單長L,入口環與相遇點距離為x,起點到環入口點的距離為a。a + x = nra + x = (n – 1)r +r = (n-1)r + L - aa = (n-1)r + (L – a – x)(L – a – x)為相遇點到環入口點的距離,由此可知,從連結清單頭到環入口點等于(n-1)循環内環+相遇點到環入口點,于是我們從連結清單頭、與相遇點分别設一個指針,每次各走一步,兩個指針必定相遇,且相遇第一點為環入口點。程式描述如下

slist* FindLoopPort(slist *head)  
{  
    slist *slow = head, *fast = head;  
  
    while ( fast && fast->next )   
    {  
        slow = slow->next;  
        fast = fast->next->next;  
        if ( slow == fast ) break;  
    }  
  
    if (fast == NULL || fast->next == NULL)  
        return NULL;  
  
    slow = head;  
    while (slow != fast)  
    {  
         slow = slow->next;  
         fast = fast->next;  
    }  
  
    return slow;  
}  
           

擴充問題:判斷兩個單連結清單是否相交,如果相交,給出相交的第一個點(兩個連結清單都不存在環)。

比較好的方法有兩個:

一、将其中一個連結清單首尾相連,檢測另外一個連結清單是否存在環,如果存在,則兩個連結清單相交,而檢測出來的依賴環入口即為相交的第一個點。

3,BITMAP

一、bitmap算法思想 

    32位機器上,一個整形,比如int a; 在記憶體中占32bit位,可以用對應的32bit位對應十進制的0-31個數,bitmap算法利用這種思想處理大量資料的排序與查詢. 

    優點:1.運算效率高,不許進行比較和移位;2.占用記憶體少,比如N=10000000;隻需占用記憶體為N/8=1250000Byte=1.25M。 

   缺點:所有的資料不能重複。即不可對重複的資料進行排序和查找。 

   比如: 

          第一個4就是 

          00000000000000000000000000010000 

          而輸入2的時候 

          00000000000000000000000000010100 

          輸入3時候 

          00000000000000000000000000011100 

          輸入1的時候 

          00000000000000000000000000011110 

    思想比較簡單,關鍵是十進制和二進制bit位需要一個map圖,把十進制的數映射到bit位。下面詳細說明這個map映射表。 

二、map映射表 

假設需要排序或者查找的總數N=10000000,那麼我們需要申請記憶體空間的大小為int a[1 + N/32],其中:a[0]在記憶體中占32為可以對應十進制數0-31,依次類推: 

bitmap表為: 

a[0]--------->0-31 

a[1]--------->32-63 

a[2]--------->64-95 

a[3]--------->96-127 

.......... 

那麼十進制數如何轉換為對應的bit位,下面介紹用位移将十進制數轉換為對應的bit位。 

三、位移轉換 

例如十進制0,對應在a[0]所占的bit為中的第一位: 

00000000000000000000000000000001 

0-31:對應在a[0]中 

i =0                        00000000000000000000000000000000 

temp=0                  00000000000000000000000000000000 

answer=1                00000000000000000000000000000001 

i =1                         00000000000000000000000000000001 

temp=1                   00000000000000000000000000000001 

answer=2                 00000000000000000000000000000010 

i =2                          00000000000000000000000000000010 

temp=2                    00000000000000000000000000000010 

answer=4                  00000000000000000000000000000100 

i =30                         00000000000000000000000000011110 

temp=30                   00000000000000000000000000011110 

answer=1073741824  01000000000000000000000000000000 

i =31                         00000000000000000000000000011111 

temp=31                   00000000000000000000000000011111 

answer=-2147483648 10000000000000000000000000000000 

32-63:對應在a[1]中 

i =32                    00000000000000000000000000100000 

temp=0                00000000000000000000000000000000 

answer=1              00000000000000000000000000000001 

i =33                     00000000000000000000000000100001 

temp=1                 00000000000000000000000000000001 

answer=2               00000000000000000000000000000010 

i =34                      00000000000000000000000000100010 

temp=2                  00000000000000000000000000000010 

answer=4                00000000000000000000000000000100 

i =61                       00000000000000000000000000111101 

temp=29                  00000000000000000000000000011101 

answer=536870912   00100000000000000000000000000000 

i =62                        00000000000000000000000000111110 

temp=30                   00000000000000000000000000011110 

answer=1073741824  01000000000000000000000000000000 

i =63                         00000000000000000000000000111111 

temp=31                   00000000000000000000000000011111 

answer=-2147483648 10000000000000000000000000000000 

淺析上面的對應表: 

1.求十進制0-N對應在數組a中的下标: 

十進制0-31,對應在a[0]中,先由十進制數n轉換為與32的餘可轉化為對應在數組a中的下标。比如n=24,那麼 n/32=0,則24對應在數組a中的下标為0。又比如n=60,那麼n/32=1,則60對應在數組a中的下标為1,同理可以計算0-N在數組a中的下标。 

2.求0-N對應0-31中的數: 

十進制0-31就對應0-31,而32-63則對應也是0-31,即給定一個數n可以通過模32求得對應0-31中的數。 

3.利用移位0-31使得對應32bit位為1. 

四、程式設計實作 

#include <stdio.h>

#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000

int a[1 + N/BITSPERWORD];//申請記憶體的大小

//set 設定所在的bit位為1
//clr 初始化所有的bit位為0
//test 測試所在的bit為是否為1

void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }
void clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }

int main()
{	int i;
	for (i = 0; i < N; i++)
		clr(i);  
	while (scanf("%d", &i) != EOF)
		set(i);
	for (i = 0; i < N; i++)
		if (test(i))
			printf("%d\n", i);

	return 0;
}
           

解析本例中的void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); } 

1.i>>SHIFT: 

其中SHIFT=5,即i右移5為,2^5=32,相當于i/32,即求出十進制i對應在數組a中的下标。比如i=20,通過i>>SHIFT=20>>5=0 可求得i=20的下标為0; 

2.i & MASK: 

其中MASK=0X1F,十六進制轉化為十進制為31,二進制為0001 1111,i&(0001 1111)相當于保留i的後5位。 

比如i=23,二進制為:0001 0111,那麼 

                         0001 0111 

                   &    0001 1111 = 0001 0111 十進制為:23 

比如i=83,二進制為:0000 0000 0101 0011,那麼 

                          0000 0000 0101 0011 

                     &   0000 0000 0001 0000 = 0000 0000 0001 0011 十進制為:19 

i & MASK相當于i%32。 

3.1<<(i & MASK) 

相當于把1左移 (i & MASK)位。 

比如(i & MASK)=20,那麼i<<20就相當于: 

         0000 0000 0000 0000 0000 0000 0000 0001 >>20 

      =0000 0000 0000 1000 0000 0000 0000 0000 

4.void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }等價于: 

void set(int i) 

   a[i/32] |= (1<<(i%32)); 

}

繼續閱讀