經典面試題(三)附答案 算法+數據結構+代碼 微軟Microsoft、谷歌Google、百度、騰訊

1.判斷單鏈表是否有環,要求空間儘量少(2011年MTK)

如何找出環的連接點在哪裏?

如何知道環的長度?

 

很經典的題目。

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 << " ";
	}
}
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章