連結串列面試筆試題目總結
連結串列是最基本的資料結構,凡是學計算機的必須的掌握的,在面試的時候經常被問到,關於連結串列的實現,百度一下就知道了。在此可以討論一下與連結串列相關的練習題。
1、在單鏈表上插入一個元素,要求時間複雜度為O(1)
解答:一般情況在連結串列中插入一元素是在末尾插入的,這樣需要從頭遍歷一次連結串列,找到末尾,時間為O(n)。要在O(1)時間插入一個新節點,可以考慮每次在頭節點後面插入,即每次插入的節點成為連結串列的第一個節點。
2、給定一個連結串列,判斷是否有環。
解答:這個是一個經典的問題了,思路也很簡單,我們首先設定兩個指標p1,p2同時指向連結串列的頭部,然後p1每次向後走1步,p2每次向後走2步
擴充套件:給定一個連結串列,找出環的入口位置。思路也是一樣,用p1,p2指標。只是需要多做一步,那就是當p1=p2的時候,將p1重新指向連結串列的頭結點,然後p1和p2都每次向後走一步,下一次p1=p2的結點就是環的入口。複雜度:時間:O(n),空間:O(1)
3、遍歷單鏈表一次,找出連結串列中間節點
解答:定義兩個指標p和q,初始都指向連結串列頭節點。然後開始向後遍歷,p每次移動2步,q移動一步,當p到達末尾的時候,p正好到達了中間位置。
4、單鏈表逆置,不允許額外分配儲存空間,不允許遞迴,可以使用臨時變數,執行時間為
解答:這個題目在面試筆試中經常碰到,基本思想上將指標逆置。如下圖所示:
實現:
Node* reverse_list(Node *head){ Node *cur=head; Node *pre = NULL; Node *post = cur->next; // Node *reverse_head = cur; while(post){ cur->next = pre; pre = cur; cur = post; post = post->next; } cur->next = pre; // reverse_head = cur; return cur; }
擴充套件:連結串列翻轉。給出一個連結串列和一個數k,比如,連結串列為1→2→3→4→5→6,k=2,則翻轉後2→1→6→5→4→3,若k=3,翻轉後3→2→1→6→5→4,若k=4,翻轉後4→3→2→1→6→5,用程式實現。
實質是也是逆置,只不過是兩個連結串列逆置後再串聯起來。實現如下:
bool rotate_list(Node *head,int k,Node* &newhead){
if(k < 0)
return false;
else if(0 == k)
return true;
int len = 0;
Node *node=head;
while(node){
++len;
node = node->next;
}
if(k > len)
return false;
Node *one_end,*two_start;
node = head;
Node *post = node->next;
int n=k;
if(1 == n){
}else{
while(n > 1){// rotate sublist one
node->next = post->next;
post->next = head;
head = post;
post = node->next;
--n;
}
}
if(len-k <= 1){ // rotate sublist two
}else{
one_end = node;
node = post;
post = post->next;
two_start = node;
n = len-k;
while(n>1){
one_end->next = post;
node->next = post->next;
post->next = two_start;
two_start = post;
post = node->next;
--n;
}
}
newhead = head;
return true;
}
5、用一個單鏈表L實現一個棧,要求push和pop的操作時間為O(1)
解答:根據棧中元素先進後出的特點,可以在連結串列的頭部進行插入和刪除操作
6、用一個單鏈表L實現一個佇列,要求enqueue和dequeue的操作時間為O(1)
解答:佇列中的元素是先進先出,在單鏈表結構中增加一個尾指標,資料從尾部入隊,從頭
部入隊。
7、給定兩個連結串列(無環),判斷是否有相交。
解答:首先明確一點,如果兩個連結串列相交,那麼從第一個交點開始到尾結點結束,所有的結點都是公共結點。所以,兩個有公共結點而部分重合的連結串列,拓撲形狀看起來像一個Y,而不可能像X。
這也就是說,如果兩個連結串列相交,那麼這兩個連結串列的尾結點肯定是公共結點,如果尾結點不是公共結點,那麼這兩個連結串列肯定不相交。
所以我們可以如下操作:依次遍歷兩個連結串列,最後判斷尾結點是否相同,如果相同,則相交,如果不相同,則不相交。複雜度:時間:O(m+n),空間:O(1)
或者一個連結串列的頭結點指向另一個連結串列的尾節點,判斷是否有環。
8、給定兩個連結串列(無環),找到第一個公共節點。
解答:我們最容易想到的是從尾結點開始挨個向前比較,最後一個相同的就是第一個公共結點。(從後往前遍歷)
但是單鏈表只能從前往後進行遍歷,如果想要從後往前的話則需要先從前向後遍歷一次,同時用棧來記錄每一個結點,最後出棧,然後挨個對比,這樣的確可行,但是卻要額外付出O(m+n)的空間,時間複雜度O(mn)。(單鏈表+棧)
仔細想想,我們可以先分別遍歷兩個單鏈表,記錄長度m和n(無妨假設m>n),然後先讓長度為m的連結串列向後走(m-n)步,接著兩個連結串列同時向後遍歷,第一個相同的結點就是要求的第一個公共結點。複雜度:O(m+n)m,n分別為兩個連結串列的長度;空間:O(1)
PS:另外還有一種巧妙的方法是把在一個連結串列尾部插入另一個連結串列,然後判斷合成的新連結串列是否有環。環入口即為第一個公共點。
擴充套件:兩個連結串列,找出他們的第一個交點,要求每個連結串列只能遍歷一次,可以對連結串列進行任何操作,空間O(1).
題目告訴說可以對連結串列進行任何操作,這是一個沒有用到的條件(大家一定要注意到題目中沒有用到的條件,往往是解題的關鍵所在)。
1.遍歷第一個連結串列List1,將每一個節點的next都置為NULL。
2.遍歷第二個連結串列List2,List2的尾節點就是第一個交點。
9、 給定2個連結串列,求這2個連結串列的並集(連結串列)和交集(連結串列)。不要求並集(連結串列)和交集(連結串列)中的元素有序。輸入:List1:10->15->4->20,List2:8->4->2->10輸出:交集(連結串列):4->10;並集(連結串列):2->8->20->4->15->10
法一:簡單直觀的方法:
InterSection(list1,list2):初始化結果連結串列為空,遍歷連結串列1,在連結串列2中查詢它的每一元素,如果連結串列2中也有這個元素,則將該元素插入到結果連結串列中。
Union(list1,list2): 初始化結果連結串列為空,將連結串列1中的所有元素都插入到結果連結串列中。遍歷連結串列2,如果結果連結串列中沒有該元素,則插入,否則跳過該元素。
法二:可適應歸併排序,not clear。
法三:Hash法
Union(list1,list2),首先用連結串列1初始化結果連結串列,建立一個空的hash表。遍歷連結串列1,將連結串列中的元素插入到hash表。然後遍歷list2,對於list2中的元素,如果hash表中不存在該元素,則同時將該元素插入到結果連結串列中,如果hash表中已經存在,則忽略該元素,繼續遍歷下一個元素。
InterSection(list1,list2),首先初始化結果連結串列為NULL,建立一個空的hash表。遍歷list1,將list1中的每一個元素都插入到hash表中。然後遍歷list2,對於list2中的元素,如果已經存在於hash表中,則將該元素插入到結果連結串列,如果不存在與hash表中,則忽略該元素,繼續遍歷下一個元素。
10、從單鏈表返回倒數第n個元素
普通,基本思路就是用棧,一一壓棧,再彈棧,第n個元素就可出來。
進階,看到棧,就應該想到遞迴,遞迴是天然的棧。用全域性變數,實現如下:
Node* pn_elem = NULL;
int nn;
void recursive(Node* node){
if(!node) return ;
recursive(node->next);
if(1==nn) pn_elem = node;
--nn;
}
高階,維護兩個指標,兩個指標相差n個元素,當前面的指標到達連結串列末尾,後面指標所指的元素即是所求的元素。實現如下
Node* last_n_elem(Node*node,int n){
if(node! || n<1) returnNULL;
Node *p=node,*q=node;
while(n>0 && q){
q=q->next;
--n;
}
while(q){
q=q->next;
p=p->next;
}
return p;
}
11、連結串列元素去重,從未排序的連結串列中移除重複的項。
思路:可使用額外的空間的話,可以用陣列存數字,實現最好的方式就是雜湊表啦。遍歷一下即可。
實現:
std::map<Node*, bool>hash;
void duplicate_remove(Node *node){
if(!node) return ;
Node *post=node->next;
hash[node->data] = true;
while(post){
if(hash[post->data]){
Node *temp = post;
post = post->next;
node->next = post;
delete temp;
}else{
hash[post->data] = true;
node = post;
post = post->next;
}
}
}
如果不允許使用臨時快取,怎麼解決?
思路:用兩個指標。當某個指標指向某個元素時,另一個指標將後面的相同元素全部刪除。複雜度O(n^2)。具體實現就不寫了。
12、連結串列求和問題。
該問題基本上有兩個型別:
a、1->2->5->4 , 2->5->3->4,得3->7->8->8.
思路:先加高位,再加低位。兩個0~9的數相加,要麼不進位,要麼進位為1.用兩個指標,p指向當前進位點,q指向當前操作點。當然第一個元素得特殊考慮,可能進位嘛。
自己實現:
Node* merge_list_add(Node *list1,Node *list2){
Node*q1=list1,*q2=list2,*ans=NULL,*pre=NULL,*p=NULL,*q=NULL;
int cvalue = q1->data+ q2->data;
bool flag = false;
ans = new Node();
// node 1
if(cvalue >9){ //進位
pre = new Node();
pre->data =cvalue%10;
ans->next = pre;
ans->data = 1;
p=pre;
}else if(9 == cvalue){//最高位為9
flag = true;
ans->data =cvalue;
pre= ans;
p=pre;
}else{
ans->data =cvalue;
p = pre= ans;
}
q1=q1->next;q2=q2->next;
while(q1 && q2){// the following node
q = new Node();
pre->next = q;
cvalue = q1->data+ q2->data;
q->data =cvalue%10;
if(cvalue > 9 ){
if(flag){
if(p != ans){
p->data += 1;
}else{//999...[],前面全是9
Node*temp = new Node();
temp->data= 1;
temp->next= ans;
flag =false;
ans =temp;
p = ans;
}
}else{
p->data +=1;
}
for(p=p->next;p!=q;p=p->next){
p->data =0;
}
}else if(cvalue <9){
p = q;
}
pre = q;
q1=q1->next;q2=q2->next;
}
return ans;
}
b、 元素個數不一定相同,高位在後,個位在連結串列頭結點。1->2->3 , 4->5->3->4,得5->7->6->4.
思路:需要注意的是,連結串列為空,有進位,連結串列長度不一樣。
#include <assert.h>
#include <iostream>
using namespace std;
struct Node{
int data;
Node *next;
};
Node* create_list(int arr[],int len){
assert(arr &&len>0);
Node *head = new Node();
head->data = arr[0];
Node *cur=NULL;
Node *pre=head;
for(int i=1;i<len;++i){
cur = new Node();
cur->data =arr[i];
pre->next = cur;
pre = cur;
}
cur->next = NULL;
return head;
}
Node* merge_list_add(Node *list1,Node *list2){
if(NULL == list1) returnlist2;
if(NULL == list2) returnlist1;
Node *ans=NULL,*pre=NULL;
int c=0;//進位
int value = 0;
while(list1 &&list2){
value =list1->data +list2->data + c;
Node* temp = newNode();
temp->data =value%10;
c = value/10;
if(pre){
pre->next =temp;
pre = temp;
}else
ans=pre=temp;
list2 = list2->next;
list1 =list1->next;
}
if(!list1 &&!list2 && c>0){//兩個連結串列長度一樣,但有進位
Node* temp = newNode();
temp->data = 1;
temp->next =NULL;//結束
pre->next = temp;
}
//有一個連結串列更長
while(list1){
value =list1->data + c;
Node* temp = newNode();
temp->data =value%10;
c = value/10;
pre->next = temp;
pre = temp;
list1 =list1->next;
}
while(list2){
value =list2->data + c;
Node* temp = newNode();
temp->data =value%10;
c = value/10;
pre->next = temp;
pre = temp;
list2 =list2->next;
}
pre->next = NULL;
return ans;
}
int main(){
int a[]={1,2,7};
int b[]={4,5,3,9,3};
//此處應該加個判斷,保證陣列元素均在[0,9]
Node* lista =create_list(a,3);
Node* listb =create_list(b,5);
Node* cur = lista;
cout<<"list a:";
while(cur != NULL){
cout<<cur->data<<"";
cur = cur->next;
}
cur = listb;
cout<<endl<<"listb: ";
while(cur != NULL){
cout<<cur->data<<"";
cur = cur->next;
}
cout<<endl;
Node *ans =merge_list_add(lista, listb);
for(; ans; ans=ans->next)
cout<<ans->data<<" ";
cout<<endl;
}
13、用演算法實現刪除連結串列的一箇中間節點,所知的只有該節點的指標。如a-b-c-d-e中只知道c的指標,實現a-b-d-e。
思路:若直接刪除的話,連結串列就斷了,可是無法得到節點c的前驅b。故可轉換思路利用c的後繼d。將d的值賦給c, 然後將後繼節點d刪除,也就實現刪除操作。
由於c的位置不定,得分情況討論。一、c為普通的中間節點,用上述方式解決。二,c為頭節點,用上述方式解決。三、c為尾節點,一般認為刪除即可,但是會出現問題。刪除之後,尾節點的前驅不為空,下次遍歷就會出錯,特別注意。四、c為空節點,直接返回。
實現:
bool remove_elem(Node* node){
if(!node || !node->next) returnfalse;
Node *post = node->next;
node->data = post->data;
node->next = post->next;
delete post;
return true;
}
擴充套件:
a、Google題目,給定單向連結串列的頭指標和一個結點指標,定義一個函式在O(1)時間刪除該結點。
思路跟前面的一致,同樣要注意尾節點。
b、只給定單鏈表中某個結點p(非空結點),在p前面插入一個結點。
思路:首先分配一個結點q,將q插入在p後,接下來將p中的資料copy入q中,
然後再將要插入的資料記錄在p中。
14、環連結串列開始節點,1->2->5->4->2
思路:
1、 用快慢指標,滿指標1,快指標2。
我們注意到第一次相遇時,指標走過的路程S1 = 非環部分長度 + 弧A長
快指標走過的路程S2 = 非環部分長度 + n * 環長 + 弧A長
S1 * 2 = S2,可得 非環部分長度 = n * 環長 - 弧A長
讓指標1到起始點後,走過一個非環部分長度,指標2過了相等的長度。
就是n * 環長 - 弧A長,正好回到環的開頭。
2、 更簡單直觀的方法就是利用雜湊表。無環的話,每個地址就是不一樣;有環的話,兩個地址一樣的就是環開始節點。下面用c++的map實現
std::map<Node*, bool>hash;
Node* loop_start(Node* node){
while(node){
if(hash(node))
return node;
else{
hash(node) = true;
node = node->next;
}
}
return NULL; //return head ; same
}
15、如何知道環的長度
一、在環上相遇後,記錄第一次相遇點為pos,之後指標slow繼續每次走1步,fast每次走2步。在下次相遇的時候fast比slow正好又多走了一圈,也就是多走的距離等於環長。
設從第一次相遇到第二次相遇,設slow走了len步,則fast走了2*len步,相遇時多走了一圈:環長=2*len-len。
二、利用雜湊表,即兩個碰撞元素間的個數16、輸入一個連結串列的頭結點,從尾到頭反過來打印出每個結點的值。
思路:用棧實現。
進階:用遞迴。實現如下:
void PrintListReversingly(ListNode*pHead){
if(pHead != NULL){
if(pHead->m_pNext != NULL){
PrintListReversingly(pHead->m_pNext);
}
printf("%d\t",pHead->m_nValue);
}
}
注意:但使用遞迴就意味著可能發生棧溢位的風險,尤其是連結串列非常長的時候。所以,基於迴圈實現的棧的魯棒性要好一些。
17、輸入兩個遞增連結串列,合併為一個遞增連結串列。
思路:遍歷兩個連結串列,依次比較,形成新的佇列
進階:遞迴,每次遞迴返回合併後新連結串列的頭結點
list_node*List::recursive_merge(list_node * a,list_node * b){
if(a == NULL)return b;
if(b == NULL)return a;
if(a->value <= b->value){
a->next=recursive_merge(a->next,b);
return a;
}
if(a->value > b->value){
b->next=recursive_merge(a,b->next);
return b;
}
}
18、用連結串列實現約瑟夫環
這裡就不實現了。
19、判斷一條單向連結串列是不是“迴文”,1-2-4-2-1
思路:對於單鏈表結構,可以用兩個指標從兩端或者中間遍歷並判斷對應字元是否相等。但這裡的關鍵就是如何朝兩個方向遍歷。
由於單鏈表是單向的,所以要向兩 個方向遍歷的話,可以採取經典的快慢指標的方法,即定位到連結串列的中間位置,再將連結串列的後半逆置,最後用兩個指標同時從連結串列頭部和中間開始同時遍歷並比較即可。
實現:(注意連結串列元素的奇偶性,稍微不同)
bool is_list_plalindrome(Node*head){
if(!head)
return false;
Node *one=head,*two=head,*pre=NULL;
while(two!=NULL && two->next!=NULL){
pre=one;
one = one->next;
two = two->next->next;
}
//if length of list is odd, mid
Node *subhead=NULL,*node=NULL,*post=NULL;
if(!two){ //even,two==NULL
subhead = one;
}else{ //odd
subhead=one->next;
}
node=subhead;
post=node->next;
while(node->next){// rotate sublist (旋轉的這種寫法,很容易理解)
node->next = post->next;
post->next = subhead;
subhead = post;
post = node->next;
}
for(Node*p=head,*q=subhead;p!=pre->next;p=p->next,q=q->next){
if(p->data!=q->data)
return false;
}
return true;
}
20、從尾到頭輸出連結串列。
題目:輸入一個連結串列的頭結點,從尾到頭反過來輸出每個結點的值。
思路:跟輸出倒數第n個元素的方法類似。
方法一、先把連結串列反向,然後再從頭到尾遍歷一遍。但該方法需要額外的操作
方法二、設一個棧,從頭到尾遍歷一次,把結點值壓力棧中,再出棧列印。
方法三、遞迴。
實現:
void list_out_reverse(Node*head){
if(!head)
return;
else
list_out_reverse(head->next);
cout<<head->data<<" ";
}
擴充套件:該題還有兩個常見的變體:
1. 從尾到頭輸出一個字串;
2. 定義一個函式求字串的長度,要求該函式體內不能宣告任何變數。
兩個的分別實現:
void reverseString(conststring& s,unsigned int begin){
if(!s.size())
return;
if(begin>=s.size())
return;
reverseString(s,begin+1);
cout<<s[begin]<<" ";
}
int getLength(const char *s){
if(*s=='/0')
return 0;
return getLength(s+1) + 1;
}
21、連結串列和陣列的區別?
分析:主要在基本概念上的理解。但是最好能考慮的全面一點,現在公司招人的競爭可能就在細節上產生,誰比較仔細,誰獲勝的機會就大。
陣列無需初始化,因為陣列的元素在記憶體的棧區,系統自動申請空間。而連結串列的結點元素在記憶體的堆區,每個元素須手動申請空間,如malloc。也就是說陣列是靜態分配記憶體,而連結串列是動態分配記憶體。連結串列如此麻煩為何還要用連結串列呢?陣列不能完全代替連結串列嗎?回到這個問題只需想想我們當初是怎麼完成學生資訊管理系統的。為何那時候要用連結串列?因為學生管理系統中的插入,刪除等操作都很靈活,而陣列則大小固定,也無法靈活高效的插入,刪除。
陣列是線性結構,靜態分配記憶體,在記憶體中連續,陣列元素在棧區。可以直接索引,時間複雜度O(1)。陣列插入或刪除元素比較困難,時間複雜度O(n)。
連結串列也是線性結構,動態分配記憶體,在記憶體中不連續,連結串列元素在堆區。元素的定位均需遍歷,時間複雜度O(n)。連結串列插入或刪除元素操作靈活性強,時間複雜度O(1)。
22、編寫實現連結串列排序的一種演算法。說明為什麼你會選擇用這樣的方法?
思路:如果只是資料內容之間的相互交換,那麼這種排序方法也比較適合連結串列的排序,插入、冒泡、希爾和選擇排序。快速排序、合併排序、堆排序都涉及到了中間值的選取問題,所以不大適合連結串列排序。
選擇排序的實現:
Node* insert_sort(Node *head){
if(!head ||!head->next)
return head;
Node *p,*q,*pre,*temp;
p=head->next;
head->next=NULL;
// p is the head of unsorted list
// head is the head of sorted list
while(p){
q=head;
while(q &&(q->data < p->data)){
pre=q;
q=q->next;
}
temp = p->next;
if(q==head){
p->next = q;
head = p;
}else{
pre->next=p;
p->next = q;
}
p=temp;
}
return head;
}
23、複雜連結串列的複製(預設無環)
Q:有一個複雜連結串列,其結點除了有一個m_pNext指標指向下一個結點外,還有一個m_pSibling指向連結串列中的任一結點或者NULL。請完成函式ComplexNode* Clone(ComplexNode* pHead),以複製一個複雜連結串列。
一開始想這道題毫無思路,如果蠻來,首先建立好正常的連結串列,然後考慮sibling這個分量,則需要O(n^2)的時間複雜度。
思路一: 一般複製一個簡單鏈表就這麼遍歷一遍就好了,這個複雜連結串列,比簡單鏈表多的地方就在於多了一個sibling的指標,也就是說在建立完簡單鏈表之後,如何在新的連結串列中找到sibling對應的地址。我們已知的是舊的節點的地址,所以只需要用一個map,儲存每一個節點舊的節點對應的新的節點的地址即可。(即將原連結串列中的結點N和相應複製結點N'建立雜湊對映<N,N'>)
第一次遍歷,建立簡單節點,第二次遍歷,對於舊連結串列中的每一個節點的sibling指標地址,從map中找到新連結串列中對應節點的地址,連線上就好了。
實現:(不錯的實現,學習)
ComplexNode* Clone(ComplexNode*pHead){
if(pHead == NULL) return NULL;
map<ComplexNode*, ComplexNode*>pointMap;
ComplexNode* newHead,*tail; // newHead指向複製的新連結串列的開頭,tail始終指向結尾
// 開闢一個頭結點
newHead = new ComplexNode;
newHead->value = pHead->value;
newHead->pNext = NULL;
newHead->pSibling = NULL;
pointMap[pHead] = newHead; // 將頭結點放入map中
tail = newHead;
ComplexNode *p = pHead->pNext;
while(p != NULL){ // 第一遍先將簡單鏈表複製一下
ComplexNode* newNode = new ComplexNode;
newNode->value = p->value;
newNode->pNext = NULL;
newNode->pSibling = NULL;
tail->pNext = newNode;
tail = newNode;
pointMap[p] = newNode;
p = p->pNext;
}
// 根據map中儲存的資料,找到對應的節點
p = pHead;
tail = newHead;
while(p!=NULL){
if(p->pSibling!=NULL){
tail->pSibling =pointMap.find(p->pSibling)->second;//Key,找N對應的N’
}
p = p->pNext;
tail = tail->pNext;
}
return newHead;
}
思路二:(精妙)一個技巧便可以巧妙的解答此題。看圖便知。
首先是原始的連結串列
然後我們還是首先複製每一個結點N為N*,不同的是我們將N*讓在對應的N後面,即為
然後我們要確定每一個N*的sibling分量,非常明顯,N的sibling分量的next就是N*的sibling分量。
最後,將整個連結串列拆分成原始連結串列和拷貝出的連結串列。
這樣,我們就解決了一個看似非常混亂和複雜的問題。
實現:
struct Node{
int val;
Node* next;
Node*sibling;
};
void Clone(Node* head){
Node*current=head;
while(current){
Node*temp=new Node;
temp->val=current->val;
temp->next=current->next;
temp->sibling=NULL;
current->next=temp;
current=temp->next;
}
}
void ConstructSibling(Node*head){
Node*origin=head;
Node*clone;
while(origin){
clone=origin->next;
if(origin->sibling)
clone->sibling=origin->sibling->next;
origin=clone->next;
}
}
Node* Split(Node* head){
Node*CloneHead,*clone,*origin;
origin=head;
if(origin){
CloneHead=origin->next;
origin->next=CloneHead->next;
origin=CloneHead->next;
clone=CloneHead;
}
while(origin){
Node*temp=origin->next;
origin->next=temp->next;
origin=origin->next;
clone->next=temp;
clone=temp;
}
return CloneHead;
}
//the whole thing
Clone(head);
ConstructSibling(head);
Split(head);
大家如果還發現其他不錯的題目,歡迎補充