鏈表一般有三種,一種是單向鏈表,每個結點只知道下家是誰;一種是雙向鏈表,每個結點知道自己的上下家;一種是循環鏈表,類似魔比斯環,可以從頭走到頭(還有一種是雙向+循環)。
差異在於增刪操作的便捷性,以及增刪後需要修改的內容(越方便操作的,需要修改的內容越多)。
這裏暫時只介紹單向鏈表
node
用來描述鏈表中數據的基本單元
Data Structure
初始化設置 next
爲 nullptr
// struct all public
struct node
{
int data;
node *next;
node()
{
next = nullptr;
}
node(int _data)
{
data = _data;
next = nullptr;
}
};
link list
採用帶頭結點的鏈表
頭結點僅作佔位,更能統一插入和刪除的函數寫法
設置一個 tail
維護最有一個結點
class linked_list
{
private:
node *head, *tail;
public:
linked_list()
{
head = new node();
tail = head;
}
};
Methods
四個主要方法
- insert
- append 隊尾
- insert_head 頭插
- insert_after
- delete
- pop_front 掐頭
- pop_back 去尾
- delete_node
- reverse
- traverse
- empty
insert_after
和 delete_node
需要判斷是否越界,所以返回值設置爲 bool
DS
class linked_list
{
private:
node *head, *tail;
public:
linked_list()
{
head = new node();
tail = head;
}
void insert_head(int _data);
bool insert_after(int index, int _data);
void append_node(int _data);
void pop_front();
bool pop_back();
bool delete_node(int index);
void reverse();
// have no impact on the linklist
bool empty();
void traverse();
};
insert
insert_head
先從最簡單的頭插入手
head->A->..
如果需要在 head
後面插入結點 tmp
,那麼需要做兩個操作
head->next = tmp
tmp->next = head->next
問題是順序如何?需要額外空間嗎?
可以將鏈表想象爲一列行駛的火車,head
相當於火車頭,一旦後面有一部分脫軌了,那麼就再也找不回來了。
所以,如果火車頭 head
不再將當前的下一個指針作爲下一列車廂,而是掛載 tmp
,這樣就會丟失數據
所以順序應該是
tmp->next = head->next
head->next = tmp
需要注意,如果維護尾指針 tail
,那麼當鏈表初始化後沒有元素時調用頭插,需要修改 tail
c++
void linked_list::insert_head(int _data){
node *tmp = new node(_data);
tmp->next = head->next;
head->next = tmp;
// if tail equals head, change tail to point to tmp
if(tail == head)
tail = tmp;
}
這裏調整下順序,先來寫鏈表的遍歷 traverse,這樣可以檢驗寫過的結果
traverse
遍歷鏈表,可以想象成一個人從車頭走到車尾,每次走到一個車廂都要記下來,接着走到下一個,走到頭了說明遍歷完畢
-
使用一個額外的指針
cur
記錄當前的車廂 -
cur->next
表示走向下一個車廂 -
cur->next == nullptr
表示遍歷完畢
c++
void linked_list::traverse(){
node *cur = head->next;
while(cur != nullptr)
{
cout << cur->data << endl;
cur = cur->next;
}
}
現在檢驗一下 insert_head 是否正確
test
int main()
{
linked_list a;
for(int i = 0; i < 10; ++i)
a.insert_head(i);
a.traverse();
}
/*
9
8
7
6
5
4
3
2
1
0
*/
append_node
在鏈表尾部插入結點,直接使用 tail
即可(tail 版本的 insert_head)
c++
void linked_list::append_node(int _data){
node *tmp = new node{_data};
tail->next = tmp;
tail = tail->next;
}
insert_after(index, _data)
插入需要插入到對應位置結點之後,默認下標從 0 開始
如果插入位置爲第一個,那麼只要調用 insert_head
即可
如果插入位置爲最後一個,那麼只要調用 append
即可
如果是中間的結點
那麼設置一個指針 pre
來找到前面的結點,cnt
記錄已經走過的結點數,需要走 index - 1
個結點
bool linked_list::insert_after(int index, int _data){
// call insert_head()
if(index == 0){
insert_head(_data);
return true;
}
int cnt = 0;
node *pre = head->next;// get the previous node of target node
while(cnt < index - 1){
pre = pre->next;
cnt++;
}
// if insert after the last node
if(pre == tail){
append_node(_data);
return true;
}
node *tmp = new node(_data);
tmp->next = pre->next;
pre->next = tmp;
return true;
}
test
int main()
{
linked_list a;
for(int i = 0; i < 10; ++i)
a.insert_head(i);
a.insert_after(10, -1);
a.insert_after(2, -2);
a.traverse();
}
/*
9
8
-2
7
6
5
4
3
2
1
0
-1
*/
no problem
delete_node (index)
刪除固定下標的元素
同 insert 相同,如果要從一列形式火車中去除一個車廂(假設可以這樣做),那麼我們需要有前一個車廂掛鉤,以及後一個車廂的位置
設置一個指針 pre
來找到前面的結點,cur
找到要刪的結點,cnt
記錄已經走過的結點,需要走 index
個車廂(同 insert 實質一致,pre 走的結點數仍然是 index - 1)
爲什麼要設置兩個指針
cur
代表當前的車廂
pre->next
代表前一節車廂
在刪除時,如果修改了前一節車廂的掛鉤 pre->next
,將它掛在了後面的車廂上,我們會失去需要刪除的車廂的位置,這是爲什麼需要兩個指針的原因(其實也可以僅使用 cur
一個指針,但是函數格式難以統一)
尾指針維護
另外,爲了維護尾指針 tail
,如果刪除的是最後一個結點,那麼需要重新修改 tail
位置
*注意,刪除尾結點後,還需要修改新的尾結點的 next
指針,可以想象成這是一位列車員,你走的每一步路都是由她指引的,當後面沒有車廂時,你要讓它知道這是最後一節車廂了,不能再往後找了
c++
bool linked_list::delete_node(int index){
if(index == 0){
pop_front();// 這裏是刪除第一個元素,函數在下文給出
return true;
}
int cnt = 0;
node *pre = head, *cur = head->next;// get the previous node of target node
while(cnt < index){
pre = pre->next;
cur = cur->next;
cnt++;
}
// if delete the tail
if(cur == tail){
tail = pre;
tail->next = nullptr;// * notice to change the next ptr
delete(cur);
return true;
}
pre->next = cur->next;
delete(cur);
return true;
}
跟 insert 相同,delete 同樣選擇對頭部和尾部單獨處理,如果考慮到刪除尾結點需要一個獨立邏輯寫一個函數,所以 delete_node
中只複用了 pop_front
,pop_back
單獨寫一個函數(不能複用的原因顯而易見)
這裏直接給出代碼,不再贅述,邏輯在 delete_node 中寫出,可以先往下看
c++
void linked_list::pop_front(){
node *tmp = head->next;
head->next = head->next->next;
delete(tmp);
}
bool linked_list::pop_back(){
if(empty()){
cerr << "the linklist is empty" << endl;
return false;
}
node *pre = head, *cur = head->next;// get the previous node of target node
while(pre->next != tail){
pre = pre->next;
cur = cur->next;
}
tail = pre;
tail->next = nullptr;
delete(cur);
return true;
}
reverse
將鏈表順序逆置
這裏的做法是,左手拿着頭結點(火車頭),右手拿着後面的結點們(車廂們),每次把 head
當做新的空鏈表頭結點,用左手的新結點做頭插,知道沒有結點可用。
可以想象成裝手槍子彈時,新到的子彈不斷把之前的子彈壓入彈匣。
尾結點維護
尾結點即當前的第一個結點 head->next
c++
void linked_list::reverse(){
if(empty()){
cerr << "the linklist is empty" << endl;
return;
}
tail = head->next;
node *cur = head->next, *next = cur->next;
while(next != nullptr){
cur->next = head->next;// the same as insert_head
head->next = cur;
cur = next;
next = next->next;
}
// dont forget the last node ~
cur->next = head->next;
head->next = cur;
tail->next = nullptr;
}
test
int main()
{
linked_list a;
for(int i = 0; i < 10; ++i)
a.insert_head(i);
a.reverse();
a.traverse();
}
/*
0
1
2
3
4
5
6
7
8
9
*/
越界問題
insert 和 delete 都有可能導致越界,指針指向位置區域
分析越界的情況
- index < 0
- index > 鏈表大小
delete 需要額外注意鏈表是否爲空
重寫 insert_after 和 delete_node
insert_after
在找到前一個車廂的位置時,如果我們發現後面沒有車廂了,那麼暫停檢查(這時已經滿足 pre == tail
)
while(cnt < index - 1 && pre != tail)
我們停下來看自己數過的車廂是不是到了數
cnt == index - 1
如果到了,那麼說明剛好在車尾,這是唯一符合的情況
不然,因爲不存在數過了的情況,只剩下
cnt < index - 1
這樣說明我們要找的車廂越界了
bool linked_list::insert_after(int index, int _data){
if(index == 0){
insert_head(_data);
return true;
}
int cnt = 0;
node *pre = head->next;// get the previous node of target node
while(cnt < index - 1 && pre != tail){
pre = pre->next;
cnt++;
}
// out of range
if(cnt < index - 1){// CHECK
cerr << "out of range" << endl;
return false;
}
// if insert after the last node
if(pre == tail){
append_node(_data);
return true;
}
node *tmp = new node(_data);
tmp->next = pre->next;
pre->next = tmp;
return true;
}
test
int main()
{
linked_list a;
for(int i = 0; i < 10; ++i)
a.insert_head(i);
a.insert_after(11, -1);
a.traverse();
}
/*
out of range
9
8
7
6
5
4
3
2
1
0
*/
delete_node
越界注意,如果我們要去掉的車廂的前一個車廂已經數到了最後一節,即 pre == tail
,但是 cnt
仍然小於 index
,那麼我們要去掉的車廂是不存在的
另外,加上一個 empty()
空鏈表不可刪的判定
bool linked_list::delete_node(int index){
if(empty()){
cerr << "the linklist is empty" << endl;
return false;
}
if(index == 0){
pop_front();
return true;
}
int cnt = 0;
node *pre = head, *cur = head->next;// get the previous node of target node
while(cnt < index && cur != tail){
pre = pre->next;
cur = cur->next;
cnt++;
}
// out of range
if(cnt < index){
cerr << "out of range" << endl;
return false;
}
// if delete the tail
if(cur == tail){
tail = pre;
tail->next = nullptr;// * notice to change the next ptr
delete(cur);
return true;
}
pre->next = cur->next;
delete(cur);
return true;
}
test
int main()
{
linked_list a;
for(int i = 0; i < 10; ++i)
a.insert_head(i);
a.delete_node(10);// exceed range
a.traverse();
}
/*
out of range
9
8
7
6
5
4
3
2
1
0
*/
完整代碼
#include<iostream>
using namespace std;
struct node
{
int data;
node *next;
node()
{
next = nullptr;
}
node(int _data)
{
data = _data;
next = nullptr;
}
};
// link list with head node
class linked_list
{
private:
node *head, *tail;
public:
linked_list()
{
head = new node();
tail = head;
}
void insert_head(int _data);
bool insert_after(int index, int _data);
void append_node(int _data);
void pop_front();
bool pop_back();
bool delete_node(int index);
// have no impact on the linklist
bool empty();
void reverse();
void traverse();
};
void linked_list::insert_head(int _data){
node *tmp = new node(_data);
tmp->next = head->next;
head->next = tmp;
if(tail == head)
tail = tmp;
}
void linked_list::append_node(int _data){
node *tmp = new node{_data};
tail->next = tmp;
tail = tail->next;
}
// index start from 0
bool linked_list::insert_after(int index, int _data){
if(index == 0){
insert_head(_data);
return true;
}
int cnt = 0;
node *pre = head->next;// get the previous node of target node
while(cnt < index - 1 && pre != tail){
pre = pre->next;
cnt++;
}
// out of range
if(cnt < index - 1){// && pre == tail
cerr << "out of range" << endl;
return false;
}
// if insert after the last node
if(pre == tail){
append_node(_data);
return true;
}
node *tmp = new node(_data);
tmp->next = pre->next;
pre->next = tmp;
return true;
}
void linked_list::pop_front(){
node *tmp = head->next;
head->next = head->next->next;
delete(tmp);
}
bool linked_list::pop_back(){
if(empty()){
cerr << "the linklist is empty" << endl;
return false;
}
node *pre = head, *cur = head->next;// get the previous node of target node
while(pre->next != tail){
pre = pre->next;
cur = cur->next;
}
tail = pre;
tail->next = nullptr;
delete(cur);
return true;
}
bool linked_list::delete_node(int index){
if(empty()){
cerr << "the linklist is empty" << endl;
return false;
}
if(index == 0){
pop_front();
return true;
}
int cnt = 0;
node *pre = head, *cur = head->next;// get the previous node of target node
while(cnt < index && cur != tail){
pre = pre->next;
cur = cur->next;
cnt++;
}
// out of range
if(cnt < index){
cerr << "out of range" << endl;
return false;
}
// if delete the tail
if(cur == tail){
tail = pre;
tail->next = nullptr;// * notice to change the next ptr
delete(cur);
return true;
}
pre->next = cur->next;
delete(cur);
return true;
}
void linked_list::reverse(){
if(empty()){
cerr << "the linklist is empty" << endl;
return;
}
tail = head->next;
node *cur = head->next, *next = cur->next;
while(next != nullptr){
cur->next = head->next;
head->next = cur;
cur = next;
next = next->next;
}
cur->next = head->next;// the same as insert_head
head->next = cur;
tail->next = nullptr;
}
bool linked_list::empty(){
if(head->next == nullptr)
return true;
return false;
}
void linked_list::traverse(){
node *cur = head->next;
while(cur != nullptr)
{
cout << cur->data << endl;
cur = cur->next;
}
}
小結
鏈表可以很形象地看做是一列行駛的列車,指針相當於每列車廂上的列車員,她來指引你走向的下一個車廂,如果你沒有及時更新,那麼你將會跳車或者在內存空間中迷路。
單向鏈表沒法找到上家,所以在刪除操作時,需要記錄上家的位置(類似歷史記錄),注意結點指針的修改順序,考慮是否要使用額外的內存空間來防止“脫鉤”的發生。
在刪除操作維護尾指針時,一個常見的錯誤是,修改了尾指針,卻沒有及時更新當前尾指針的 next
域,這會導致在遍歷時訪問已經刪除的結點,及在無垠的內存空間中迷路,tail->next = nullptr
非常重要。
關於雙向鏈表,由於知道了結點上下家,從頭和尾尋找結點都比較方便,只是修改時比較麻煩,之後應該會更新。