如何找出環的連接點在哪裏?
如何知道環的長度?
很經典的題目。
1.判斷是否有環。使用兩個指針。一個每次前進1,另一個每次前進2,且都從鏈表第一個元素開始。顯然,如果有環,兩個指針必然會相遇。
2.環的長度。記下第一次的相遇點,這個指針再次從相遇點出發,直到第二次相遇。此時,步長爲1的指針所走的步數恰好就是環的長度。
3.環的鏈接點。記下第一次的相遇點,使一個指針指向這個相遇點,另一個指針指向鏈表第一個元素。然後,兩個指針同步前進,且步長都爲1。當兩個指針相遇時所指的點就是環的連接點。
鏈接點這個很不明顯,下面解釋一下。
如圖,設鏈表不在環上的結點有a個,在環上的結點有b個,前兩個指針第一次在第x個結點相遇。
S( i )表示經過的總步長爲i之後,所訪問到的結點。
顯然,環的鏈接點位S( a ),即從起點經過a步之後所到達的結點。
現在要證明:
從第一次的相遇點x再經過a步之後可到達鏈接點S( a ),即 S( x + a ) = S( a )
由環的週期性可知,只要 a = tb 其中( t = 1, 2, …. ),則S( x + a ) = S( a )
如何證明a = tb?
再看看已知條件,當兩個指針第一次相遇時,必有S( x ) = S( 2x )
由環的週期性可知,必有 2x = x + bt, 即x = tb.
struct Node
{
int data;
Node* next;
Node( int value ): data(value), next(NULL) {};
};
//判斷單鏈表是否有環
bool IsCircle( Node *pHead )
{
//空指針 或 只有一個元素且next爲空時,必無環
if( pHead == NULL || pHead->next == NULL ) return false;
Node *pSlow = pHead;
Node *pFast = pHead;
while( ( pFast != NULL ) && ( pFast->next != NULL ) )
{
//分別按步長1、2前進
pSlow = pSlow->next;
pFast = pFast->next->next;
if( pSlow == pFast ) break;
}
if( ( pFast == NULL ) || ( pFast->next == NULL ) )
return false;
else
return true;
}
//求環的長度
int GetLen( Node *pHead )
{
if( pHead == NULL || pHead->next == NULL ) return false;
Node *pSlow = pHead;
Node *pFast = pHead;
//求相遇點
while( ( pFast != NULL ) && ( pFast->next != NULL ) )
{
pSlow = pSlow->next;
pFast = pFast->next->next;
if( pSlow == pFast ) break;
}
//計算長度
int cnt = 0;
while( ( pFast != NULL ) && ( pFast->next != NULL ) )
{
pSlow = pSlow->next;
pFast = pFast->next->next;
cnt++;
//再次相遇時,累計的步數就是環的長度
if( pSlow == pFast ) break;
}
return cnt;
}
//求環的入口點
Node* GetEntrance( Node* pHead )
{
if( pHead == NULL || pHead->next == NULL ) return false;
Node *pSlow = pHead;
Node *pFast = pHead;
//求相遇點
while( ( pFast != NULL ) && ( pFast->next != NULL ) )
{
pSlow = pSlow->next;
pFast = pFast->next->next;
if( pSlow == pFast ) break;
}
pSlow = pHead;
while( pSlow != pFast )
{
//同步前進
pSlow = pSlow->next;
pFast = pFast->next;
}
return pSlow;
}
2.用非遞歸的方式合併兩個有序鏈表(2011年MTK)
用遞歸的方式合併兩個有序鏈表
基本的鏈表操作,沒什麼好說的。
非遞歸:就是把一個鏈表上的所有結點插入到另一個鏈表中。
遞歸:??
//兩個有序鏈表的合併
Node* merge( Node* pHeadA, Node* pHeadB )
{
//處理空指針
if( pHeadA == NULL || pHeadB == NULL )
{
return ( pHeadA == NULL ) ? pHeadB : pHeadA;
}
//處理第一個節點
Node *px, *py;
if( pHeadA->data <= pHeadB->data )
{
px = pHeadA; py = pHeadB;
}
else
{
px = pHeadB; py = pHeadA;
}
Node *pResult = px;
//將py上的節點按順序插入到px
Node *pre = px;
px = px->next;
while( py != NULL && px != NULL )
{
//在px上找到py應該插入的位置
while( py != NULL && px != NULL && py->data > px->data )
{
py = py->next;
px = px->next;
pre = pre->next;
}
//py插入到pre和px之間
if( py != NULL && px != NULL )
{
//py指針前移
Node* tmp = py;
py = py->next;
//pre指針前移
Node* tmpPre = pre;
pre = pre->next;
//插入
tmp->next = px;
tmpPre->next = tmp;
//px指針前移
px = px->next;
}
else
break;
}
if( px == NULL ) pre->next = py;
return pResult;
}
4編程實現:把十進制數(long型)分別以二進制和十六進制形式輸出,不能使用printf系列
用位操作實現。十進制數在計算機裏本來就是按二進制存儲的,因此通過掩碼和移位操作很容易輸出二進制形式。這裏,要注意的一點:對最高位符號位的處理。符號位應該單獨處理,否則結果會出錯。十六進制的處理和二進制基本相同,只是每次處理四位。
void LongFormat( long value )
{
//處理符號位
long mask = 0x1 << ( 8 * sizeof(long) - 1 );
if( value & mask ) cout << "1";
else cout << "0";
//轉換爲二進制
mask = 0x1 << ( 8 * sizeof(long) - 2 );
for( int i=1; i<8*sizeof(long); i++ )
{
if( value & mask ) cout << "1";
else cout << "0";
mask >>= 1;
}
cout << endl;
//處理符號位
cout << "0x";
mask = 0xF << ( 8 * sizeof(long) - 4 );
long tmp = ( value & mask ) >> ( 8 * sizeof(long) - 4 );
if( tmp < 10 )
cout << tmp;
else
cout << (char)( 'a' + ( tmp - 10 ) );
//轉換爲十六進制
mask = 0xF << ( 8 * sizeof(long) - 8 );
for( int i=1; i<2*sizeof(long); i++ )
{
tmp = ( value & mask ) >> ( 8 * sizeof(long) - 4 * i - 4 );
if( tmp < 10 )
cout << tmp;
else
cout << (char)( 'a' + ( tmp - 10 ) );
mask >>= 4;
}
}
5.編程實現:找出兩個字符串中最大公共子字符串,如"abccade","dgcadde"的最大子串爲"cad"
有人說:可用KMP。可惜KMP忘了,找時間補一下。
還有人說:用兩個字符串,一個作行、一個作列,形成一個矩陣。相同的位置填1,不同的位置填0。然後找哪個斜線方向上1最多,就可以得到最大公共子字符串。空間複製度0( m*n ),感覺時間上也差不多O( m*n )
沒想到什麼好辦法,只會用最笨的辦法O( m*n )。即,對於字符串A中的每個字符,在字符串B中找以它爲首的最大子串。哎,即便是這個最笨的方法,也寫了好長時間,汗。
void GetSubStr( char *strA, char *strB, char *ans )
{
int max = 0;
char *pAns = NULL;
//遍歷字符串A
for( int i=0; *(strA+i) != '\0'; i++ )
{
//保存strB的首地址,每次都從strB的第一個元素開始比較
char *pb = strB;
while( *pb != '\0' )
{
//保存strA的首地址
char *pa = strA + i;
int cnt = 0;
char *pBegin = pb;
//如果找到一個相等的元素
if( *pb == *pa )
{
while( *pb == *pa && *pb != '\0' )
{
pa++;
pb++;
cnt++;
}
if( cnt > max )
{
max = cnt;
pAns = pBegin;
}
if( *pb == '\0' ) break;
}
else
pb++;
}
}
//返回結果
memcpy( ans, pAns, max );
*(ans+max) = '\0';
}
6.有雙向循環鏈表結點定義爲:
struct node
{
int data;
struct node *front,*next;
};
有兩個雙向循環鏈表A,B,知道其頭指針爲:pHeadA,pHeadB,請寫一函數將兩鏈表中data值相同的結點刪除。
沒什麼NB算法。就是遍歷對鏈表A,對A的每個元素,看它是否在鏈表B中出現。如果在B中出現,則把所有的出現全部刪除,同時也在A中刪除這個元素。
思路很簡單,實現起來也挺麻煩。畢竟,雙向循環鏈表也算是線性數據結構中最複雜的了。如何判斷雙向循環鏈表的最後一個元素?p->next == pHead.
刪除操作:
雙向循環鏈表只有一個節點時
雙向循環鏈表至少有兩個節點時
struct Node
{
int data;
struct Node *front,*next;
Node( int value ): data( value ), front( NULL ), next( NULL ) { };
void SetPointer( Node *pPre, Node *pNext ) { front = pPre; next = pNext; };
};
//如果成功刪除返回真。否則,返回假。
bool DeleteValue( Node *&pHead, int target )
{
if( pHead == NULL ) return false;
//至少有兩個元素
bool flag = false;
Node* ph = pHead;
while( ph->next != pHead )
{
Node *pPre = ph->front;
Node *pNext = ph->next;
if( ph->data == target )
{
//如果刪除的是第一個元素
if( ph == pHead ) pHead = ph->next;
pPre->next = pNext;
pNext->front = pPre;
Node *tmp = ph;
delete tmp;
//設置刪除標記
flag = true;
}
ph = pNext;
}
//只有一個元素或最後一個元素
if( ph->next == pHead )
{
if( ph->data == target )
{
//如果要刪除的是最後一個元素
if( ph->front != ph )
{
Node *pPre = ph->front;
Node *pNext = ph->next;
pPre->next = pNext;
pNext->front = pPre;
Node *tmp = ph;
delete tmp;
}
else
{
delete pHead;
pHead = NULL;
}
flag = true;
}
}
return flag;
}
void DeleteSame( Node *&pHeadA, Node *&pHeadB )
{
if( pHeadA != NULL && pHeadB != NULL )
{
Node *pa = pHeadA;
while( pa->next != pHeadA )
{
//如果B中含有pa->data,並且已經刪除
if( DeleteValue( pHeadB, pa->data ) )
{
//在A中刪除pa->data
Node *tmp = pa->next;
DeleteValue( pHeadA, pa->data );
pa = tmp;
}
else
pa = pa->next;
}
//只有一個元素或最後一個元素
if( DeleteValue( pHeadB, pa->data ) )
{
DeleteValue( pHeadA, pa->data );
}
}
}
7.設計函數int atoi(char *s)。
int i=(j=4,k=8,l=16,m=32); printf(“%d”, i); 輸出是多少?
解釋局部變量、全局變量和靜態變量的含義。
解釋堆和棧的區別。
論述含參數的宏與函數的優缺點。
1.字符串轉整形,嘿嘿,前面已寫過了。
2.逗號表達式的值等於最後一個逗號之後的表達式的值。對應本題,即i=(m=32)
3.局部變量:在函數內定義的變量。作用域範圍:只在定義它的塊內有效。
全局變量:在函數之外定義的變量。作用域範圍:從定義的地方開始直到文件末尾都有效。
靜態變量:static變量,屬於靜態存儲方式。靜態局部變量在函數內定義,生存期是整個源代碼。但是,作用域範圍只在定義它的函數內有效。靜態全局變量與一般的全局變量:一般全局變量在整個源程序內有效,靜態全局變量只在所在文件內有效。
4.堆:一般new出來的變量都在堆裏,這裏變量要由程序員自己管理,即在不用的時候要及時釋放,防止內存泄露。
棧:一般局部變量、函數的參數都在棧裏,他們是由編譯器來自動管理的。
8.順時針打印矩陣
題目:輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每一個數字。
例如:如果輸入如下矩陣:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
則依次打印出數字, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10。
分析:包括Autodesk、EMC在內的多家公司在面試或者筆試裏採用過這道題
本來想寫遞歸的,結果遞歸的終止條件比較複雜。因爲每次把最外面一圈都出來了,所以矩形的行列都減小2,而且還要記錄當前矩形的起始位置。遞歸終止條件,要考慮行列爲0、1的情況。哎,想不清楚。最後還是非遞歸的好寫。也很簡單,沒啥所的,直接看代碼把。
const int MAX_ROW = 100;
const int MAX_COL = 100;
void PrintMatrix( int data[][MAX_COL], int row, int col )
{
int top = 0;
int bottom = row-1;
int left = 0;
int right = col-1;
int cnt = 0;
int total = row * col;
while( cnt < total )
{
//從左到右,打印最上面一行
int j;
for( j=left; j<=right && cnt<total; j++ )
{
cout << data[top][j] <<" ";
cnt++;
}
top++;
//從上到下,打印最右面一列
for( j=top; j<=bottom && cnt<total; j++ )
{
cout << data[j][right] << " ";
cnt++;
}
right--;
//從右到左,打印最下面一行
for( j=right; j>=left && cnt<total; j-- )
{
cout << data[bottom][j] << " ";
cnt++;
}
bottom--;
//從下到上,打印最左邊一列
for( j=bottom; j>=top && cnt<total; j-- )
{
cout << data[j][left] << " ";
cnt++;
}
left++;
}
}
9.對稱子字符串的最大長度
題目:輸入一個字符串,輸出該字符串中對稱的子字符串的最大長度。
比如輸入字符串“google”,由於該字符串裏最長的對稱子字符串是“goog”,因此輸出。
分析:可能很多人都寫過判斷一個字符串是不是對稱的函數,這個題目可以看成是該函數的加強版
10.用1、2、3、4、5、6這六個數字,寫一個main函數,打印出所有不同的排列,如:512234、412345等,要求:"4"不能在第三位,"3"與"5"不能相連.
先不考慮限制條件,我們可以用遞歸打印出所有的排列(嘿嘿,這個前面寫過,可以用遞歸處理)。然後,只要在遞歸終止時,把限制條件加上,這樣只把滿足條件的排列打印出來,就可以了。
bool IsValid( char *str )
{
for( int i=1; *(str+i) != '\0'; i++ )
{
if( i == 2 && *(str+i) == '4' ) return false;
if( *(str+i) == '3' && *(str+i-1) == '5' || *(str+i) == '5' && *(str+i-1) == '3' )
return false;
}
return true;
}
void PrintStr( char *str, char *start )
{
if( str == NULL ) return;
if( *start == '\0' )
{
if( IsValid( str ) ) cout << str << endl;
}
for( char *ptmp = start; *ptmp != '\0'; ptmp++ )
{
char tmp = *start;
*start = *ptmp;
*ptmp = tmp;
PrintStr( str, start+1 );
tmp = *start;
*start = *ptmp;
*ptmp = tmp;
}
}
11。微軟面試題
一個有序數列,序列中的每一個值都能夠被2或者3或者5所整除,1是這個序列的第一個元素。求第1500個值是多少?
2、3、5的最小公倍數是30。[ 1, 30]內符合條件的數有22個。如果能看出[ 31, 60]內也有22個符合條件的數,那問題就容易解決了。也就是說,這些數具有週期性,且週期爲30.
第1500個數是:1500/22=68 1500%68=4。也就是說:第1500個數相當於經過了68個週期,然後再取下一個週期內的第4個數。一個週期內的前4個數:2,3,4,5。
故,結果爲68*30=2040+5=2045
12.從尾到頭輸出鏈表
題目:輸入一個鏈表的頭結點,從尾到頭反過來輸出每個結點的值。鏈表結點定義如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
分析:這是一道很有意思的面試題。該題以及它的變體經常出現在各大公司的面試、筆試題中。
鏈表的反向輸出。前面我們討論過:鏈表的逆序,使用3個額外指針,遍歷一遍鏈表即可完成。這裏當然可以先把鏈表逆序,然後再輸出。鏈表上使用遞歸一般也很簡單,雖然遞歸要壓棧,但程序看起來很簡潔。
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
void PrintReverse( ListNode* pHead )
{
ListNode* ph = pHead;
if( ph != NULL )
{
PrintReverse( ph->m_pNext );
cout << ph->m_nKey << " ";
}
}