本文主要介紹一些解決單向連結串列上部分操作問題的思路和程式碼實現。
主要的問題包括以下幾點:
- 向單向連結串列中插入一個節點
- 刪除單向連結串列中的一個節點
- 查詢單向連結串列中的一個節點
- 擴充套件問題1:查詢單向連結串列中的倒數第k個節點。
- 擴充套件問題2:查詢單向連結串列中的中間節點,當節點總個數為偶數時返回中間兩個元素中的前者(後者)
- 反轉單向連結串列(非遞迴實現)
- 反轉單向連結串列(遞迴實現)
- 判斷單向連結串列是否有環
- 判斷兩個單向連結串列是否相交
- 擴充套件問題:返回兩個連結串列的第一個交點。
- 用單連結串列實現棧,要求push和pop的時間複雜度為O(1)
- 用單連結串列實現佇列,要求enQueue和deQueue的時間複雜度為O(1)
- 在一個連結串列中刪除另一個連結串列中的元素(即求差集(A-B))
由上一篇文章可知,從資料的底層儲存角度來講,資料結構可分順序表(陣列)和連結串列兩種。因此,要掌握好資料結構,首先就要掌握好對陣列和連結串列的操作。
在本篇文章中,主要先介紹一些連結串列的基本操作。
一個連結串列又可以有好多種表現形式,它可以是單向的或雙向的,也可以是排序的或者未排序,或者是環形的和非環形的。下面主要介紹一些連結串列的常見的操作。
在解決生活或計算機中的任何問題時“三思而後行”的道理都是非常實用的。只有想明白了,要做什麼、怎麼做,然後再著手去做,才能比較容易地解決問題。如果連要做什麼,怎麼做都不清楚,那現在所做的行動能成功的概論就非常小了。因此,要用計算機編碼來解決一些問題的時候,不要急於編碼,而是要先想清楚思路和注意點,然後再著手實現自己的思路。
在處理連結串列問題時的公共注意點有:
- 要時刻考慮指標是否為空
- 不要把指標指向一些無效的地址空間。
- 如果沒要求,操作的過程中儘量不要破壞連結串列的原始結構;如果破壞連結串列的結構是目前較好的一種實現方式,那麼處理完資料後,一定要記得還原連結串列的原始資料結構。
下面介紹一些常見的單向連結串列上的操作問題。
單向連結串列節點定義:
1 2 3 4 5 6 7 8 9 10 11 12 |
//單向連結串列上的節點定義 class SingleList { public: int data; SingleList *next; //指向下一個結點的指標 SingleList() { next = NULL; } }; |
1 向單向連結串列中插入一個節點
思路:如圖1所示,向連結串列中插入一個節點。
圖1 插入節點
如果已知一個節點指標pre和一個節點指標cur,要把cur插入到pre節點之後,很顯然要保證連結串列不會斷開而丟失後面的節點,要先把後面的節點指標(指向lat的指標)儲存下來,即有cur->next = pre->next,然後把cur連線的一串連結串列連線到pre後面,即pre->next = cur;
上面介紹了,在一個節點之後插入節點的情況。這是通常的情況。如果要向一個連結串列的頭部插入節點,就只需要將新節點的下一個指標指向連結串列的頭指標即可。
在這種情況下,有兩點要注意:
- 連結串列是否為空連結串列
- 要插入的節點是不是空指標。
程式碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//向單連結串列中插入一個節點(插入在鏈開始處) //輸入引數:單連結串列的頭指標和要插入的節點指標 //輸出引數:無 //返回值:指向單連結串列的頭指標 SingleList* Insert(SingleList *head,SingleList *node) { if(node == NULL) { return head; } else if(head == NULL) { return node; } node->next = head; head = node; return head; } |
2 刪除單向連結串列中的一個節點
思路:
圖2 刪除節點cur
如圖2所示,要刪除節點cur就像要在環環相扣的鏈上去掉一環一樣, 我們只需要將前面的環和當前環所連線的下一個環相連就可以了。
針對這情況,我只需要從頭遍歷連結串列,找到當前節點的前一節點pre,然後令pre->next = cur->next。因為要從頭遍歷連結串列所以刪除操作的時候時間複雜度是O(n)。
但是,在有些情況下,如果要求時間複雜度必須為O(1),我們又能怎麼做呢?想一下,如果要求時間複雜度為O(1)的話,肯定不能再遍歷連結串列,而是在當地做幾個小步聚,那麼做什麼才能刪除呢?
如果不能找到cur的前一個節點,那我們是不是可以像陣列中的覆蓋一樣用後一個節點的值把當前節點值覆蓋,然後刪除後一個節點呢。當然可以,如圖3所示。但是,這種方法存在一個侷限,就是如果要刪除的節點是尾節點的話,即沒有可以替代它被刪除的點時,我們就只能按上面的迴圈遍歷查詢前一個節點了。
圖3 刪除節點cur(不知道頭指標時)
在刪除連結串列中的節點時,要特別注意連結串列的頭節點和尾節點。
程式碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/在單連結串列中刪除一個節點 //輸入引數:單連結串列的頭指標和要刪除的節點 //輸出引數:無 //返回值:指連結串列的頭指標 SingleList* Delete(SingleList *head,SingleList *node) { SingleList *pSL; //連結串列為空或要刪除的結點為空 if((head == NULL)||(node == NULL)) { return head; } //如果刪除的不是尾節點 if(node->next != NULL) { pSL = node->next; node->next = node->next->next; node->data = pSL->data; delete pSL; pSL = NULL; return head; } //如果刪除的是尾節點 else { pSL = head; while((pSL->next != NULL)&&(pSL->next != node)) { pSL = pSL->next; } if(pSL->next != NULL) { pSL->next = pSL->next->next; delete node; } return head; } } |
3 查詢單向連結串列中的一個節點
思路:
這個問題比較簡單,只需要在從頭一步一步的逐個判斷節點值是不是要找的值就可以了。
程式碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//在單連結串列中查詢一個元素 //輸入引數:單連結串列的頭指標和要查詢的元素值 //輸出引數:無 //返回值:指向元素的節點或NULL SingleList* Select(SingleList *head,int data) { while(head) { if(head->data == data) { return head; } head = head->next; } return head; } |
擴充套件問題1:查詢單向連結串列中的倒數第k個節點。
思路:按照遍歷查詢連結串列的常用模式,可能會馬上想到一種簡單的方法就是:先遍歷連結串列,看一下連結串列中有多少個節點。假設連結串列中有n個節點,那麼要找倒數第k個,也就是正數n-k+1個了。接下來,只要指標從頭向後走n-k步就可了。這種方法大約要執行2n-k次。
那麼,有沒有比上面的方法更高效的方法呢?把思維開啟,不要固定在遍歷連結串列的時候只能用有一個指標的思維定式上。我們可以嘗試用多個指標以不同的次序開始遍歷連結串列。
對於,這個問題,我們就可以設定兩個指標,一個指標先在連結串列上走K步,然後另一個指標從連結串列頭開始和前一個指標一起向後走,這樣當第一個指標到達連結串列尾部時候,第二個指標就會在第一個指標前面k個結點處。這時就找到了倒數第K個節點,演算法執行的次數為n 次,比上一個方法減少了一些步,如圖4。
圖4 查詢倒數第k個節點
程式碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//返回連結串列中的倒數第K節點的指標 //輸入引數:單連結串列的頭指標,要查詢的節點的倒數位置 //輸出引數:無 //返回值:成功返回節點指標,失敗返回NULL SingleList* returnNodeFromBack(SingleList* head,int k) { SingleList *firstPtr,*secondPtr; int count = 0; firstPtr = secondPtr = head; while((firstPtr)&&(count < k)) { firstPtr = firstPtr->next; count++; } if(count < k) { return NULL; } while(firstPtr) { firstPtr = firstPtr->next; secondPtr = secondPtr->next; } return secondPtr; } |
擴充套件問題2:查詢單向連結串列中的中間節點,當節點個數為偶數時返回中間兩個元素中的前者(後者)
思路:類似於第一個擴充套件問題,對於查詢中間元素,我們首先也可以利用先遍佈連結串列看一看總共有多少個節點,然後再走節點總個數的一半即可找到中間元素。顯然,這種方法也是要遍歷兩次連結串列。
那麼,能不能借鑑上面的改進方法再來改進一下這個問題呢。當然可以,在此問題中依然使用兩個遍歷指標。讓第一個指標每次走兩步,第二個指標每次走一步,這樣因為第一個指標經過的節點數是第二指標的兩倍,所以當第一個指標到達中點時,第二個指標正好處於連結串列的中間位置。
程式碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/返回連結串列的中間節點 //輸入引數:單連結串列的頭指標 //輸出引數:無 //返回值:中間節點的指標或NULL SingleList* returnMidNode(SingleList* head) { SingleList *firstPtr,*secondPtr; firstPtr = secondPtr = head; //連結串列中沒有節點或只有一個節點 if((firstPtr == NULL) || (firstPtr->next == NULL)) { return firstPtr; } //while((firstPtr)&&(firstPtr->next)) //偶數個數時返回中間兩個索引較大者 while((firstPtr->next)&&(firstPtr->next->next))//偶數個數時返回中間兩個索引較小者 { firstPtr = firstPtr->next->next; secondPtr = secondPtr->next; } return secondPtr; } |
舉一反三,將方法推廣,我們也可以很容易地找到連結串列中前三分之一位置上的數。