鏈表的存儲方式使得它能夠高效的在指定位置插入與刪除,時間複雜度均爲 O(1)。c++
在結點 p 以後增長一個結點 q 總共分三步:面試
申請一段內存用以存儲 q (可使用內存池避免頻繁申請和銷燬內存)。
將 p 的指針域數據複製到 q 的指針域。
更新 p 的指針域爲 q 的地址。spa
刪除結點 p 以後的結點 q 總共分兩步:指針
將 q 的指針域複製到 p 的指針域。
釋放 q 結點的內存。code
鏈表的主要代碼blog
#include <bits/stdc++.h>
using namespace std;
//定義一個結點模板
template<typename T>
struct Node {
T data;
Node *next;
Node() : next(nullptr) {}
Node(const T &d) : data(d), next(nullptr) {}
};
//刪除 p 結點後面的元素
template<typename T>
void Remove(Node<T> *p) {
if (p == nullptr || p->next == nullptr) {
return;
}
auto tmp = p->next->next;
delete p->next;
p->next = tmp;
}
//在 p 結點後面插入元素
template<typename T>
void Insert(Node<T> *p, const T &data) {
auto tmp = new Node<T>(data);
tmp->next = p->next;
p->next = tmp;
}
//遍歷鏈表
template<typename T, typename V>
void Walk(Node<T> *p, const V &vistor) {
while(p != nullptr) {
vistor(p);
p = p->next;
}
}
int main() {
auto p = new Node<int>(1);
Insert(p, 2);
int sum = 0;
Walk(p, [&sum](const Node<int> *p) -> void { sum += p->data; });
cout << sum << endl;
Remove(p);
sum = 0;
Walk(p, [&sum](const Node<int> *p) -> void { sum += p->data; });
cout << sum << endl;
return 0;
}
面試問題總結
沒法高效獲取長度,沒法根據偏移快速訪問元素,是鏈表的兩個劣勢。然而面試的時候常常遇見諸如獲取倒數第k個元素,獲取中間位置的元素,判斷鏈表是否存在環,判斷環的長度等和長度與位置有關的問題。這些問題均可以經過靈活運用雙指針來解決。ip
Tips:雙指針並非固定的公式,而是一種思惟方式~內存
先來看"倒數第k個元素的問題"。設有兩個指針 p 和 q,初始時均指向頭結點。首先,先讓 p 沿着 next 移動 k 次。此時,p 指向第 k+1個結點,q 指向頭節點,兩個指針的距離爲 k 。而後,同時移動 p 和 q,直到 p 指向空,此時 q 即指向倒數第 k 個結點。能夠參考下圖來理解:get
class Solution { public: ListNode* getKthFromEnd(ListNode* head, int k) { ListNode *p = head, *q = head; //初始化
while(k--) { //將 p指針移動 k 次
p = p->next; } while(p != nullptr) {//同時移動,直到 p == nullptr
p = p->next; q = q->next; } return q; } };
獲取中間元素的問題。設有兩個指針 fast 和 slow,初始時指向頭節點。每次移動時,fast向後走兩次,slow向後走一次,直到 fast 沒法向後走兩次。這使得在每輪移動以後。fast 和 slow 的距離就會增長一。設鏈表有 n 個元素,那麼最多移動 n/2 輪。當 n 爲奇數時,slow 剛好指向中間結點,當 n 爲 偶數時,slow 剛好指向中間兩個結點的靠後一個(能夠考慮下如何使其指向前一個結點呢?)。it
下述代碼實現了 n 爲偶數時慢指針指向靠後結點。
class Solution { public: ListNode* middleNode(ListNode* head) { ListNode *p = head, *q = head; while(q != nullptr && q->next != nullptr) { p = p->next; q = q->next->next; } return p; } };
是否存在環的問題。若是將尾結點的 next 指針指向其餘任意一個結點,那麼鏈表就存在了一個環。
上一部分中,總結快慢指針的特性 —— 每輪移動以後二者的距離會加一。下面會繼續用該特性解決環的問題。
當一個鏈表有環時,快慢指針都會陷入環中進行無限次移動,而後變成了追及問題。想象一下在操場跑步的場景,只要一直跑下去,快的總會追上慢的。當兩個指針都進入環後,每輪移動使得慢指針到快指針的距離增長一,同時快指針到慢指針的距離也減小一,只要一直移動下去,快指針總會追上慢指針。
根據上述表述得出,若是一個鏈表存在環,那麼快慢指針必然會相遇。實現代碼以下:
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while(fast != nullptr) {
fast = fast->next;
if(fast != nullptr) {
fast = fast->next;
}
if(fast == slow) {
return true;
}
slow = slow->next;
}
return false;
}
};
最後一個問題,若是存在環,如何判斷環的長度呢?方法是,快慢指針相遇後繼續移動,直到第二次相遇。兩次相遇間的移動次數即爲環的長度。