Leetcode鏈表類算法題目分析總結與C++ Python實現
鏈表是常見的數據結構,一般由關鍵字和成員指針構成,爲動態集合提供了一種簡單而靈活的表示方法,其中單鏈表,由於結構簡單而問題變化多樣,在程序員技術筆、面試中最爲常見,可以作爲試金石快速考察被面試者的coding能力。筆者整理了leetcode和麪試中常見的鏈表算法題,分類分析思路,力求舉一反三與融會貫通,同時給出C++、Python的實現,方便大家夯實算法基礎與準備算法刷題。
0. 鏈表的定義
- 鏈表與數組比較,都是線性存儲結構,區別是數組的線性順序由下標決定,鏈表的順序是由各個對象裏的指針決定。
- 雙向鏈表的每個元素都是一個對象(C++ Primer中對對象的解釋爲具有類型的內存區域),每個對象有一個關鍵字和兩個指針,還可以包含其他的數據。
- 鏈表分爲單鏈接(singly linked)的和雙鏈接(double linked)的,已排序或未排序。
- 鏈表元素的實現如下,
C++代碼
//C++版鏈表結點定義
struct Node {
Node(int data, Node* next=nullptr) {
v=data;
n=next;
}
int v;
Node* n;
}
python代碼
#python
#python3類定義可以省略顯式繼承object
class Node():
def __init__(self, data, next=None):
self.val=data
#python無指針,變量均爲引用形式,引用可以近似爲指針,用引用代替指針
self.next=next
1. 單鏈表的基本操作:建、查、插、改、刪
1.1 鏈表的創建有頭插法和尾插法兩種
/* params
pHead:鏈表頭指針的指針,創建的鏈表作爲參數返回,若使用指針則必須使用引用形式;也可以將頭指針做函數返回值
n: 鏈表長度
pdata: 若非空,則將數組元素順序作爲關鍵字
*/
void CreateLinklist(Node **pHead, int n, int *pdata = nullptr) {
if (n < 1) {
return;
}
if (pdata) {
*pHead = new Node(pdata[0]);
} else {
*pHead = new Node(0);
}
Node *p = *pHead;
for (int i = 1; i < n; i++) {
Node *tmp;
if (pdata) {
tmp = new Node(pdata[i]);
} else {
tmp = new Node(i);
}
p->n = tmp;
p = tmp;
}
}
//頭插法
Node *CreateLinklist(int data[], int n) {
if (!data) {
return nullptr;
}
Node *rear = new Node(data[n - 1]);
Node *head;
//頭插法
for (int i = n - 2; i >= 0; i--) {
head = new Node(data[i]);
head->n = rear;
rear = head;
}
return head;
}
1.2 鏈表的查找
Node *ListSearch(Node* head, int key) {
Node *p=head;
while(p){
if(p->v==key){
return p;
}
p=p->n;
}
return p;
}
1.3 鏈表的插入、刪除
void InsertNode(Node **pHead, int Index, int Value) {
if (Index < 0) {
return;
}
Node *tmp = new Node(Value);
if (Index == 0) {
tmp->n = *pHead;
*pHead = tmp;
return;
}
int cnt = Index;
Node *p = *pHead;
while (cnt > 1 && p) {
p = p->n;
cnt--;
}
tmp->n = p->n;
p->n = tmp;
}
//刪除鏈表位置Index所在的節點
void DeleteNode(Node **pHead, int Index) {
if (Index == 1) {
Node *p = *pHead;
*pHead = (*pHead)->n;
delete p;
return;
}
if (Index > 1) {
int cnt = Index;
Node *p = *pHead;
while (cnt > 2 && p) {
p = p->n;
cnt--;
}
Node *cur = p->n;
p->n = cur->n;
delete cur;
}
}
//刪除整個鏈表
void DeleteLinkedList(Node **pHead) {
if (*pHead == nullptr) {
return;
}
Node *p = *pHead;
Node *tmp = *pHead;
while (p) {
tmp = p;
p = p->n;
delete tmp;
}
}
2. 常見單鏈表算法題
建議選擇一個簡單數據,按照代碼逐行分析變量,畫出鏈表圖分析,熟能生巧。
2.1 題目:反轉一個單鏈表
可以採用遞歸思路實現,將當前結點以後的鏈表完成反轉,再把當前結點的下一個結點的next指向當前結點,當前結點的next指針置空。
Node *ReverseLinkedList(Node *head) {
if (!head || !head->n) {
return head;
}
Node *newhead = ReverseLinkedList(head->n);
head->n->n = head;
head->n = nullptr;
return newhead;
}
考慮非遞歸方式如何實現,可以想象在平地上移動一艘船該如何做呢,取用多個圓木,依次墊底,從後往前放置,爲了能夠修改整個鏈表的順序需要三個指針來完成這個目的。
//對參數傳入的鏈表進行逆轉
void ReverseLinkedList(Node **pHead) {
if (*pHead == nullptr || (*pHead)->n == nullptr) {
return;
}
Node *pre = nullptr;
Node *cur = *pHead;
Node *n = (*pHead)->n;
while (cur) {
n = cur->n;
cur->n = pre;
pre = cur;
cur = n;
}
*pHead = pre;
}
用三個指針當作傳遞的圓木流動,也是解決鏈表的插入排序問題一個方法。
2.2 題目:鏈表上實現插入排序(Leetcode )
插入排序的基本思路是,每次從未排序的右側元素去一個,插入到已經排好序的左側的適當位置,相比對數組做插入排序,更加直觀;需要考慮待插入元素是最小的,即要把這個結點放到頭,所以需要構造一個關鍵字爲無窮小的頭指針dum
,每次循環前pre
置爲頭指針,cur
爲排好序的部分的尾結點,lat
爲待插入元素,循環中pre
移動到待插入結點的左側,並用tmp
標出插入位置,插入結點,並保持鏈表完整。
void InsertSort(Node *&pHead) {
if (!pHead || !pHead->n) {
return;
}
Node *dum = new Node(INT_MIN);
dum->n = pHead;
Node *pre = dum;
Node *cur = pHead;
Node *lat = pHead->n;
while (cur) {
lat = cur->n;
if (lat && cur->v > lat->v) {
while (pre->n && pre->n->v < lat->v) {
pre = pre->n;
}
Node *tmp = pre->n;
pre->n = lat;
cur->n = lat->n;
lat->n = tmp;
} else {
cur = lat;
}
pre = dum;
}
pHead = dum->n;
}
2.3 兩個鏈表做十進制加法
Node *AddTwoNumbers(Node *l1, Node *l2) {
Node *dummy = new Node(0), *p = dummy;
int carry = 0;
while (l1 || l2 || carry) {
if (l1) {
carry += l1->v;
l1 = l1->n;
}
if (l2) {
carry += l2->v;
l2 = l2->n;
}
p->n = new Node(carry % 10);
carry /= 10;
p = p->n;
}
return dummy->n;
}