鏈表常見問題

鏈表的存儲方式使得它能夠高效的在指定位置插入與刪除,時間複雜度均爲 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;
    }
};


最後一個問題,若是存在環,如何判斷環的長度呢?方法是,快慢指針相遇後繼續移動,直到第二次相遇。兩次相遇間的移動次數即爲環的長度。

相關文章
相關標籤/搜索