三.從零寫雙鏈表到基本演算法的實現(終)
阿新 • • 發佈:2019-01-22
一.雙鏈表的引入和基本實現
1.雙鏈表的結構
首先,我們要明白雙鏈表並不是有兩條鏈的連結串列,而是有兩個遍歷方向的連結串列,因此我們所說的雙鏈表其實就是雙向連結串列的簡稱。
2.有效資料+2個指標的節點(雙鏈表)
(1)單鏈表的節點 = 有效資料 + 指標(指標指向後一個節點)
(2)雙向連結串列的節點 = 有效資料 + 2個指標(一個指向後一個節點,另一個指向前一個節點)
(3)雙鏈表的結構圖如下:
==可以看出,圖中的每一個節點都有一個有效資料和兩個指標(前向指標pPrev和後向指標pNext),分別指向該節點的前一個節點和後一個節點。頭結點的pPrev和尾節點的pNext都指向NULL==
3.建立一個雙鏈表節點的實現
根據上面的雙鏈表的結構圖,再結合前面學習的單鏈表,建立雙鏈表的節點無疑依葫蘆畫瓢!
①.實現一個連結串列的首要任務就是構造節點,在c語言中構構造節點的方法就是定義一個結構體:
// 構建一個雙鏈表的節點
struct node
{
int data; // 有效資料
struct node *pPrev; //指向上一個節點的指標
struct node *pNext; // 指向下一個節點的指標
};
②.使用堆記憶體建立一個節點
因為連結串列的記憶體要求比較靈活,不能用棧,也不能用data資料段。只能用堆記憶體
建立節點的過程:
①申請一個節點大小的堆記憶體
②檢查堆記憶體是否申請成功
③清理申請到的堆記憶體
④填充節點中的資料
⑤節點中的兩個指標域初始化為NULL;
4.程式碼的具體實現
// 作用:建立一個連結串列節點
// 返回值:指標,指標指向我們本函式新建立的一個節點的首地址
struct node * create_node(int data)
{
struct node *p = (struct node *)malloc(sizeof(struct node));
if (NULL == p)
{
printf("malloc error.\n" );
return NULL;
}
// 清理申請到的堆記憶體
bzero(p, sizeof(struct node));
// 填充節點
p->data = data;
p->pPrev = NULL;//預設建立的節點的前向後向指標都指向NULL
p->pNext = NULL;
return p;
}
二.雙鏈表的演算法之插入節點
1.尾部插入方式
==和單鏈表的插入方式基本一樣,所以,對單鏈表的插入 遍歷 刪除 掌握ok,這裡分析起來是很簡單的。==
①.直接上手尾插入節點的連結串列分析圖
尾部插入節點的任務分析:
==思路:將從連結串列尾部插入節點的任務分為兩步:==
1.1.第一步.找到連結串列的尾節點
1.2.第二步.將新節點接到連結串列的尾節點後面成為新的尾節點
①.原來的尾節點的pNext指標指向新節點的首地址
②.新節點的pPrev指標指向原來的尾節點的首地址
1.3程式碼實現
// 將新節點new插入到連結串列pH的尾部
void insert_tail(struct node *pH, struct node *new)
{
// 第一步先走到連結串列的尾節點
struct node *p = pH;
while (NULL != p->pNext)
{
p = p->pNext; // 第一次迴圈走過了頭節點
}
// 迴圈結束後p就指向了原來的最後一個節點
// 第二步:將新節點插入到原來的尾節點的後面
p->pNext = new; // 後向指標關聯好了。新節點的地址和前節點的next
new->pPrev = p; // 前向指標關聯好了。新節點的prev和前節點的地址
// 前節點的prev和新節點的next指標未變動
}
/**************雙鏈表插入節點***********************/
int main(void)
{
struct node *pHeader = create_node(0); // 頭指標
insert_tail(pHeader, create_node(11));//尾巴插入雙鏈表節點
insert_tail(pHeader, create_node(12));//尾巴插入雙鏈表節點
insert_tail(pHeader, create_node(13));//尾巴插入雙鏈表節點
// 遍歷
printf("node 1 data: %d.\n", pHeader->pNext->data);//通過pNext指標訪問每個節點
printf("node 2 data: %d.\n", pHeader->pNext->pNext->data);
printf("node 3 data: %d.\n", pHeader->pNext->pNext->pNext->data);
struct node *p = pHeader->pNext->pNext->pNext; // p指向了最後一個節點
printf("node 3 data: %d.\n", p->data);//通過pPrev指標逆向訪問每個節點
printf("node 2 data: %d.\n", p->pPrev->data);
printf("node 1 data: %d.\n", p->pPrev->pPrev->data);
return 0;
}
}
2.頭部插入方式
2.1.頭結點插入的重要四個步驟:
①.新節點的pNext指向原來的第一個節點的首地址,即圖中的新節點pNext和原來的第一個節點的首地址相連
②.原來第1個有效節點的prev指標指向新節點的首地址
③.頭節點的pNext指標指向新節點的首地址
④.頭節點的pNext指向新節點的首地址,即圖中頭結點的首地址和新節點的pPrev相連
2.2.思考:四個步驟①②③④是否可以交換一下順序?(可參考單鏈表頭部插入)
==答案顯然是不能的,因為由上圖可知,如果交換①和③的步驟,③是可以完成的,但是執行步驟①時就會發現,原來的第一個有效節點的地址已經丟失了,由圖中知道,我們原來第一個節點的首地址是在頭結點的pNext指標中儲存的,因為先執行了步驟③,故原來第一個節點的有效地址丟失了。且②必須放在①③中間,具體原因和上面一致。==
2.3.程式碼實現
// 將新節點new前頭插入連結串列pH中。
// 演算法參照圖示進行連線,一共有4個指標需要賦值。注意的是順序。
void insert_head(struct node *pH, struct node *new)
{
// 新節點的next指標指向原來的第1個有效節點的地址
new->pNext = pH->pNext;
// 原來第1個有效節點的prev指標指向新節點的地址
if (NULL != pH->pNext)
//因為當只有頭結點和頭指標時,即ph->pNext=NULL;
// 而pH->pNext->pPrev,則會報段錯誤
pH->pNext->pPrev = new;
// 頭節點的next指標指向新節點地址
pH->pNext = new;
// 新節點的prev指標指向頭節點的地址
new->pPrev = pH;
}
需要注意的是,第二步中,當連結串列如果只有一個頭結點,即沒有有效節點時,
②.原來第1個有效節點的prev指標指向新節點的首地址
這個步驟則會報段錯誤,因為當只有頭結點和頭指標時,即ph->pNext=NULL;而pH->pNext->pPrev,則會報段錯誤
==故解決方法就是新增這條if (NULL != pH->pNext)語句,判斷是否為連結串列頭結點,如果是,則不做任何操作。==
三.雙鏈表的演算法之遍歷節點
==(1)雙鏈表是單鏈表的一個父集。雙鏈表中如何完全無視pPrev指標,則雙鏈表就變成了單鏈表。這就決定了雙鏈表的正向遍歷(後向遍歷)和單鏈表是完全相同的。==
==(2)雙鏈表中因為多了pPrev指標,因此雙鏈表還可以前向遍歷(從連結串列的尾節點向前面依次遍歷直到頭節點)。但是前向遍歷的意義並不大,主要是因為很少有當前當了尾節點需要前向遍歷的情況。==
1.正向遍歷
1.1因為正向遍歷和單鏈表的過程相同,這裡不再贅述。
遍歷方法:==從頭指標+頭節點開始,順著連結串列掛接指標依次訪問連結串列的各個節點,取出這個節點的資料,然後再往下一個節點,直到最後一個節點,結束返回。==
1.2正向遍歷雙鏈表遍歷節點過程分析圖(cp的單鏈表分析)
1.3程式碼實現
//正向遍歷(後向遍歷)雙鏈表,ph為指向單鏈表的頭指標,將遍歷的節點資料打印出來
void bianli(struct node *ph)
{
struct node *p=ph; //頭指標的後面是頭節點
printf("---------正向遍歷----------\n");
printf("---------start----------\n");
while(NULL!=p->pNext)// 是不是最後一個節點
{
p=p->pNext;// 走到下一個節點,也就是迴圈增量
printf("node data: %d.\n",p->data);
}
printf("------------end----------\n");
}
2.反向遍歷
==反向遍歷節點(即從尾節點開始前向遍歷)的邏輯和正向遍歷差不多,通過p=p->pPrev來向前移動,依次訪問節點。==
//反向遍歷(前向遍歷)雙鏈表,ph為指向單鏈表的頭指標,將遍歷的節點資料打印出來
void fanxiang_bianli(struct node *pTail)
{
struct node *p=pTail; //頭指標的後面是頭節點
printf("---------反向遍歷----------\n");
printf("---------start----------\n");
while(NULL!=p->pPrev)// 是不是最後一個節點
{
printf("node data: %d.\n",p->data);/*若printf與下面交換程式,則把尾節點的資料捨棄了,並沒有打印出來,所以,遍歷節點一定的注意順序!*/
p=p->pPrev;// 走到下一個節點,也就是迴圈增量
}
printf("------------end----------\n");
}
/**************雙鏈表之遍歷節點***********************/
int main(void)
{
struct node *pHeader = create_node(0); // 頭指標
insert_tail(pHeader, create_node(11));//尾插入雙鏈表節點
insert_tail(pHeader, create_node(12));//尾插入雙鏈表節點
insert_tail(pHeader, create_node(13));//尾插入雙鏈表節點
bianli(pHeader);//正向遍歷雙鏈表
struct node *p=pHeader->pNext->pNext->pNext;//p指向最後一個節點
fanxiang_bianli(p);//反向遍歷雙鏈表
/*// 手動遍歷
printf("node 1 data: %d.\n", pHeader->pNext->data);//通過pNext指標訪問每個節點
printf("node 2 data: %d.\n", pHeader->pNext->pNext->data);
printf("node 3 data: %d.\n", pHeader->pNext->pNext->pNext->data);
struct node *p = pHeader->pNext->pNext->pNext; // p指向了最後一個節點
printf("node 3 data: %d.\n", p->data);//通過pPrev指標逆向訪問每個節點
printf("node 2 data: %d.\n", p->pPrev->data);
printf("node 1 data: %d.\n", p->pPrev->pPrev->data);
*/
return 0;
}
四.雙鏈表的演算法之刪除節點
1.為什麼要刪除節點?
(1)一直在強調,連結串列到底用來幹嘛的?==用來儲存資料==
(2)有時候連結串列節點中的資料不想要了,因此要刪掉這個節點。
2、刪除節點的2個步驟
(1)第一步:找到要刪除的節點;第二步:刪除這個節點。
3、如何找到待刪除的節點
(1)通過遍歷來查詢節點。從頭指標+頭節點開始,順著連結串列依次將各個節點拿
出來,按照一定的方法比對,找到我們要刪除的那個節點。
4、如何刪除一個節點(分兩種情況)(這裡就和單鏈表不同)
(1)待刪除的節點是尾節點的情況:
==這種情況要刪除節點2就需要斷開①.②.這兩條指標的連結,然後釋放free(p)==
步驟1.首先把把待刪除的尾節點的前一個節點的pNext指標存放的待刪除尾節點的首地址清除,然後把待刪除的尾節點的前一個節點的pNext指標指向null(這時候就相當於原來尾節點前面的一個節點變成了新的尾節點),即圖中①
步驟2.然後再將待刪除尾節點的pPrev指標存放的上一個節點的地址斷開連結,即圖②
步驟3.最後再將這個摘出來的節點free掉即可。。 (因為最終是要釋放尾節點的,所以,第②步可以省略)
==p表示當前節點地址,p->pNext表示後一個節點地址,p->pPrev表示前一個節點的地址
故步驟①表示為:p->pPrev->pNext = NULL;
步驟②表示為p->pPrev = NULL;
(2)待刪除的節點不是尾節點的情況:
==這種情況要刪除節點1就需要斷開①.②.③.④這四條指標的連結,然後釋放free(p)==
步驟如下:
步驟①.首先把待刪除節點的前一個節點的pNext指標指向待刪除節點的後一個節點的首地址(這樣就把這個節點從連結串列中摘出來了),
步驟②.當前待刪除的節點的prev和next指標置為NULL,但這裡可以不用管,因為後面會整體銷燬整個節點
步驟③.待刪除節點的後一個節點的prev指標指向待刪除節點的前一個節點的首地址
步驟④.最後再將這個摘出來的節點free掉即可。(因為最終是要釋放尾節點的,所以,第②步可以省略)
==p表示當前節點地址,p->pNext表示後一個節點地址,p->pPrev表示前一個節點的地址==
故步驟①為前一個節點的next指標指向後一個節點的首地址
表示為:==p->pPrev->pNext = p->pNext;==
步驟②.③當前待刪除的節點的prev和next指標置為NULL
==p->pPrev = NULL;==
==p->pNext = NULL;==
步驟④為後一個節點的prev指標指向前一個節點的首地址
表示為==p->pNext->pPrev = p->pPrev;==
==和待刪除的節點是尾節點同樣,最終都要釋放free(p),所以第②步和第③步可以省略。==
5、設計一個刪除節點的演算法
(1)刪除節點的演算法流程框圖如下
(2)總結總體步驟如下:
①通過遍歷連結串列來尋找需要刪除的節點
②找到該節點又分為兩種情況:一個是非尾節點,另一個是尾節點
③通過上面描述的待刪除的節點不同處理方法處理:
6.程式碼實現
// 從雙鏈表pH中刪除節點,待刪除的節點的特徵是資料區等於data
// 返回值:當找到並且成功刪除了節點則返回0,當未找到節點時返回-1
//struct node *ph:頭指標
//int data:待刪除節點的有效資料
int delete_node(struct node *ph,int data)
{
struct node *p=ph; //頭指標的後面是頭節點
printf("---------start----------\n");
if(NULL==p)//這是為了防止沒有頭結點,報段錯誤
{
return -1;
}
while(NULL!=p->pNext)// 通過遍歷節點,判斷是不是尾節點
{
//p->pNext表示下一個節點的地址,即走到下一個節點
p=p->pNext;
//判斷這個節點是否為我們要刪除的節點
if(p->data==data)
{
//處理找到的節點,分為兩種情況
if(NULL==p->pNext)//待刪除的節點如果為尾節點,執行如下
{
//p表示當前節點地址,p->pNext表示後一個節點地址,
//p->pPrev表示前一個節點的地址
//把把待刪除的尾節點的前一個節點的pNext指標存放的待刪除尾節點的首地址清除,然後把待刪除的尾節點的前一個節點的pNext指標指向null
p->pPrev->pNext = NULL;
//p->pPrev = NULL;//待刪除的尾節點的前一個節點的pPrev指標指向null
//這裡可以省略這部,因為下面整個待刪除節點都被銷燬了
free(p);//釋放待刪除的尾節點的記憶體
}
else //待刪除的節點如果為非尾節點,即普通節點,執行如下
{
// 待刪除的節點的前一個節點的next
//指標指向待刪除的節點的後一個節點的首地址
p->pPrev->pNext = p->pNext;
// 當前節點的prev和next指標都不用管,因為後面會整體銷燬整個節點
p->pNext->pPrev = p->pPrev;//待刪除的節點的後一個節點的prev指標指向前一個節點的首地址
free(p);//釋放待刪除的尾節點的記憶體
}
printf("------------end----------\n");
return 0;//成功刪除該節點,退出程式返回0
}
}
printf("-----------沒有找到該待刪除的節點----------\n");
return -1;
}
/**************雙鏈表之刪除節點***********************/
int main(void)
{
struct node *pHeader = create_node(0); // 頭指標
insert_tail(pHeader, create_node(11));//尾插入雙鏈表節點
insert_tail(pHeader, create_node(12));//尾插入雙鏈表節點
insert_tail(pHeader, create_node(13));//尾插入雙鏈表節點
bianli(pHeader);//正向遍歷雙鏈表
delete_node(pHeader, 12);//從連結串列pH中刪除資料為33的節點
printf("------------------刪除該節點後-------------\n");
//再次正向遍歷雙鏈表各個節點的有效資料
bianli(pHeader);
/*// 手動遍歷
printf("node 1 data: %d.\n", pHeader->pNext->data);//通過pNext指標訪問每個節點
printf("node 2 data: %d.\n", pHeader->pNext->pNext->data);
printf("node 3 data: %d.\n", pHeader->pNext->pNext->pNext->data);
struct node *p = pHeader->pNext->pNext->pNext; // p指向了最後一個節點
printf("node 3 data: %d.\n", p->data);//通過pPrev指標逆向訪問每個節點
printf("node 2 data: %d.\n", p->pPrev->data);
printf("node 1 data: %d.\n", p->pPrev->pPrev->data);
*/
return 0;
}