數據結構與算法專題之線性表——鏈表(二)雙向鏈表

  本文是線性表之鏈表第二彈——雙向鏈表。在學習本章節之前,應該首先學習並掌握鏈表的概念及單鏈表的原理和實現,還未學習的小夥伴請移步上一篇文章,傳送門:

  數據結構與算法專題之線性表——鏈表(一)單鏈表




  能看到這裏,我就默認你學會了單鏈表並且理解了鏈表的基本概念,下面我們開始雙向鏈表的學習。

雙向鏈表的概念及結構

基本概念

  雙向鏈表,顧名思義,就是擁有前後兩個方向的鏈表。由前面的單鏈表學習可知,單鏈表只有一個指向後置元素的指針域,也就決定了單鏈表只可以從前向後遍歷反之則不行。所以看到這裏大家可能已經想到了,雙向鏈表與單鏈表結構的基本區別就是多了一個指向前置結點的指針域,沒錯就是這樣。

  雙向鏈表有着更爲靈活的訪問方式,我們C++ STL內置的list實際上就是一個雙向鏈表。

結點結構

  參照單鏈表,我們可以把雙向鏈表的結點結構定義如下圖:


  可見,雙向鏈表需定義兩個指針域,分別指向前置結點和後置結點,代碼如下:

template<class T>
struct DNode
{
    T data;
    DNode<T> *next, *previous;
};

類結構

  與單鏈表類似,雙向鏈表包含如下幾個基本操作:

   (1) 尾部插入:push_back

   (2) 頭部插入:push_front

   (3) 任意位置插入:insert

   (4) 刪除index位置元素: del

   (5) 獲取任意位置元素:get(這裏我們依然是獲取元素指針)

   (6) 獲取指定元素第一次出現的位置:find

  類的結構代碼如下:

template<class T>
class DList
{
private:
    DNode<T> *head, *tail;
    int cnt;
public:
    DList()
    {
        head = new DNode<T>;
        tail = head;
        head->next = NULL;
        head->previous = NULL;
        cnt = 0;
    }
    void push_back(T elem); // 尾部插入
    void push_front(T elem); // 頭部插入
    void insert(T elem, int index);  // 向index位置插入
    void del(int index); // 刪除index位置的元素
    DNode<T>* get(int index); // 獲取index位置的元素指針
    DNode<T>* find(T elem); // 獲取elem第一次出現的位置的元素指針
    int size(); // 獲取鏈表的大小
    void each(char split, int method); // 遍歷輸出鏈表,以split參數爲間隔,method爲0正向,爲1反向,默認0
};
  可以看出,與單鏈表操作基本相同,這裏我們加一個特別的,就是根據元素值來找元素指針,而不是根據索引找元素指針。

  下面我們根據上述定義來依次講解各操作,如果你需要其他的一些操作,可以在學習了本章以後自己動手編碼實現~

雙向鏈表的實現

1. 尾部插入(push_back)

  與單鏈表類似,需要先實例化一個結點p,併爲結點next指針域賦NULL,如下圖:


  然後將tail的next域指向p,p的previous指向tail,使得它倆相互鏈接,如圖:


  最後,將tail指針指向p即可,如圖:


  至此,完成了雙鏈表尾部插入操作,很簡單吧?代碼如下:

template<class T>
void DList<T>::push_back(T elem) // 尾部插入
{
    DNode<T> *p = new DNode<T>;
    p->data = elem;
    p->next = NULL;
    p->previous = tail;
    tail->next = p;
    tail = p;
    cnt++;
}

2. 頭部插入(push_front)

  頭部插入,與單鏈表也是類似的,同樣第一步需要實例化一個結點p並賦值,這裏不畫圖了,第二步開始,由於是頭部插入,所以元素應該插在頭結點和首元素結點之間,我們應該先讓p結點的next域指向首元素結點,previous域指向頭結點,把結點的兩個指針域想象成“雙手”,也就是讓p的“雙手”“牽住”前後兩個結點。但注意紅色標記的文字部分,如果鏈表是空的,則不存在元素0,也就是說p的next域應賦值空,但是這裏我們無需特別判斷,只需執行p->next=head->next,因爲如果鏈表是空的話 ,head->next恰好是NULL,如圖所示:


  然後,只要p的“雙手”牽住了頭結點和元素0結點,我們就不怕他們“跑掉”了,然後我們再讓頭結點的next域指向p,元素0結點的previous域指向p這裏需要注意的是,如果插入前鏈表爲空,則不存在元素0,也就不存在紅色標記文字的那一步,但是,記得移動尾指針!看圖!


  特殊情況!與單鏈表一樣,插入前無元素的,需要在插入後移動尾指針!看圖:


  好了,頭部插入完成,上代碼:

template<class T>
void DList<T>::push_front(T elem) // 頭部插入
{
    DNode<T> *p = new DNode<T>;
    p->data = elem;
    p->next = head->next; // 右手牽着head的next
    p->previous = head; // 左手牽着head
    if(head->next != NULL) // head後有結點
    {
        head->next->previous = p; // head後置結點的前置結點改爲p
    }
    head->next = p;
    if(cnt == 0) // 無元素,移動尾指針
    {
        tail = p;
    }
    cnt++;
}

3. 任意位置插入(insert)

  向任意位置插入,同樣找出兩種特殊情況,假設鏈表長度爲n,即在0位置插入和在n位置插入爲特殊情況,0插入相當於push_front,n插入相當於push_back(關於爲啥是n而不是n-1,在前面單鏈表的insert那裏已經介紹過了,自己移步去看~),其他屬通常情況。

  假設鏈表含有n個元素,要在位置i插入元素(i != 0 && i != n),如圖所示:


  插入方式與頭部插入差不多,而且這是雙向鏈表,元素指針的不僅可以向後移動,還可以向前移動,靈活的很~所以在位置i插入,實際上就是插在元素i-1和元素i之間,同樣,先搞一個ptr指針初始化爲head,向後移動i次即指向i-1元素,圖不畫了與單鏈表那個差不多,然後將新元素p分別與i-1和i“牽手”,如圖:


  然後,p都主動牽手了,i和i-1你們倆幹啥呢?所以他們倆的手就要分開,去牽p的手咯,看圖:


  好啦,牽手成功,皆大歡喜了,上代碼:

template<class T>
void DList<T>::insert(T elem, int index)  // 向index位置插入
{
    if(index <= 0) // 索引小於等於0都插在位置0
    {
        this.push_front(elem);
        return;
    }
    else if(index >= cnt) // 索引大於等於cnt都插在末尾
    {
        this.push_back(elem);
        return;
    }
    DNode<T> *ptr = head;
    int i = index;
    while(i--)
    {
        ptr = ptr->next;
    }
    DNode<T> *p = new DNode<T>;
    p->data = elem;
    p->next = ptr->next; // 右手牽i結點
    p->previous = ptr; // 左手牽i-1結點
    ptr->next->previous = p; // i-1分別牽p結點
    ptr->next = p; // i結點牽p
    cnt++;
}

4. 任意位置刪除(del)

  假設我們要刪除元素i,那麼我們就需要將元素i的前置和後置結點“牽手”,拋棄i結點,然後釋放內存。與單鏈表不同的是,改變i-1結點的next域後,假如存在i+1結點,還需要將i+1結點的previous域置爲i-1。如果刪除的元素是最後一個的話,同樣,需要移動尾指針 。

  注意!我!不!想!畫!圖!了!!!能把Windows畫圖軟件用的這麼6的,也只有我了吧哈哈哈。

  這裏我們只要注意下指針域的完整就可以了,直接上代碼,步驟我在註釋裏,大家可以憑藉高超的想象力,參考單鏈表的圖自行在腦海中繪圖:

template<class T>
void DList<T>::del(int index) // 刪除index位置的元素
{
    if(index < 0 || index >= cnt)
    {
        return;
    }
    DNode<T> *ptr = head;
    int i = index;
    while(i--)
    {
        ptr = ptr->next;
    }
    DNode<T> *p = ptr->next; // 獲取待刪元素指針
    ptr->next = p->next;
    if(p->next != NULL) // 待刪元素不是最後一個
    {
        p->next->previous = ptr;
    }
    delete p;
    if(index == cnt - 1) // 如果刪除的元素是最後一個
    {
        tail = ptr; // 修改尾指針指向爲ptr(刪除元素的前置)
    }
    cnt--;
}

5. 獲取任意索引位置元素指針(get)

  同單鏈表,沒什麼特別的地方,初始化一個指針ptr指向head->next,循環index次即可,注意判斷溢出的index。

  代碼:

template<class T>
DNode<T>* DList<T>::get(int index) // 獲取index位置的元素指針
{
    if(index >= cnt) // 如果給定位置過大,則默認返回最後元素
    {
        index = cnt - 1;
    }
    if(index < 0) // 如果給定位置過小,則默認返回首元素
    {
        index = 0;
    }
    int i = index;
    DNode<T> *ptr = head->next; // 指針指向首元素
    while(i--)
    {
        ptr = ptr->next;
    }
    return ptr;
}

6. 獲取指定元素第一次出現的元素指針(find)

  這裏是新添的一個功能,我讓它的返回值是指針,大家也可以修改代碼讓其返回索引(重載一個也行),總之大家想怎麼寫就這麼寫,這裏只是參考,由於這裏是根據元素值查找元素所在位置,所以我們需要遍歷整個鏈表,遇到匹配元素就返回指針,如果到了鏈表尾還未找到,直接返回NULL。

  代碼如下:

template<class T>
DNode<T>* DList<T>::find(T elem) // 獲取elem第一次出現的位置的元素指針
{
    DNode<T> *ptr = head->next;
    while(ptr)
    {
        if(ptr->data == elem)
        {
            // 找到匹配元素,跳出循環,ptr指向該元素
            break;
        }
        ptr = ptr->next;
    }
    // 循環正常結束說明未找到,此時ptr==NULL
    return ptr;
}

7. 獲取鏈表大小(size)

  直接返回私有字段cnt,其實就是個getter啊,沒啥好說的,代碼:

template<class T>
int DList<T>::size() // 獲取鏈表的大小
{
    return cnt;
}

8. 遍歷鏈表(each)

  這個遍歷鏈表我添加了一個參數,因爲是雙向鏈表,單向遍歷體現不出它的優勢,所以加了個參數讓它可以雙向遍歷,method爲0時正向遍歷,爲1或其他值時反向遍歷,默認爲0。需要注意的是,我們正向遍歷的結束點是指針爲空時結束,而反向遍歷卻不是,爲啥呢?別忘了我們有頭結點!所以反向遍歷的結束標誌是head!下面給出起點終點。

  正向:head->next ===> NULL

  反向:tail ===> head

  多說無益,看代碼理解:

template<class T>
void DList<T>::each(char split, int method = 0) // 遍歷輸出鏈表,以split參數爲間隔,method爲0正向,爲1反向,默認0
{
    // 根據method初始化遍歷的起點
    DNode<T> *p = method ? tail : head->next;
    while(p &&  p != head) // 結束條件: p爲NULL,即到達鏈表尾;或者p等於head,即到達鏈表頭
    {
        cout<<(p->data);
        p = method ? p->previous : p->next; // 根據method決定遊標指針的移動方向
        putchar(p == NULL || p == head ? '\n' : split); // 輸出換行或分隔符
    }
}

**完整版雙向鏈表代碼,附測試用例

#include <bits/stdc++.h>

using namespace std;

template<class T>
struct DNode
{
    T data;
    DNode<T> *next, *previous;
};

template<class T>
class DList
{
private:
    DNode<T> *head, *tail;
    int cnt;
public:
    DList()
    {
        head = new DNode<T>;
        tail = head;
        head->next = NULL;
        head->previous = NULL;
        cnt = 0;
    }
    void push_back(T elem); // 尾部插入
    void push_front(T elem); // 頭部插入
    void insert(T elem, int index);  // 向index位置插入
    void del(int index); // 刪除index位置的元素
    DNode<T>* get(int index); // 獲取index位置的元素指針
    DNode<T>* find(T elem); // 獲取elem第一次出現的位置的元素指針
    int size(); // 獲取鏈表的大小
    void each(char split, int method); // 遍歷輸出鏈表,以split參數爲間隔,method爲0正向,爲1反向,默認0
};

template<class T>
void DList<T>::push_back(T elem) // 尾部插入
{
    DNode<T> *p = new DNode<T>;
    p->data = elem;
    p->next = NULL;
    p->previous = tail;
    tail->next = p;
    tail = p;
    cnt++;
}
template<class T>
void DList<T>::push_front(T elem) // 頭部插入
{
    DNode<T> *p = new DNode<T>;
    p->data = elem;
    p->next = head->next; // 右手牽着head的next
    p->previous = head; // 左手牽着head
    if(head->next != NULL) // head後有結點
    {
        head->next->previous = p; // head後置結點的前置結點改爲p
    }
    head->next = p;
    if(cnt == 0) // 無元素,移動尾指針
    {
        tail = p;
    }
    cnt++;
}
template<class T>
void DList<T>::insert(T elem, int index)  // 向index位置插入
{
    if(index <= 0) // 索引小於等於0都插在位置0
    {
        this.push_front(elem);
        return;
    }
    else if(index >= cnt) // 索引大於等於cnt都插在末尾
    {
        this.push_back(elem);
        return;
    }
    DNode<T> *ptr = head;
    int i = index;
    while(i--)
    {
        ptr = ptr->next;
    }
    DNode<T> *p = new DNode<T>;
    p->data = elem;
    p->next = ptr->next; // 右手牽i結點
    p->previous = ptr; // 左手牽i-1結點
    ptr->next->previous = p; // i-1分別牽p結點
    ptr->next = p; // i結點牽p
    cnt++;
}
template<class T>
void DList<T>::del(int index) // 刪除index位置的元素
{
    if(index < 0 || index >= cnt) // 非法位置,忽略
    {
        return;
    }
    DNode<T> *ptr = head;
    int i = index;
    while(i--)
    {
        ptr = ptr->next;
    }
    DNode<T> *p = ptr->next; // 獲取待刪元素指針
    ptr->next = p->next;
    if(p->next != NULL) // 待刪元素不是最後一個
    {
        p->next->previous = ptr;
    }
    delete p;
    if(index == cnt - 1) // 如果刪除的元素是最後一個
    {
        tail = ptr; // 修改尾指針指向爲ptr(刪除元素的前置)
    }
    cnt--;
}
template<class T>
DNode<T>* DList<T>::get(int index) // 獲取index位置的元素指針
{
    if(index >= cnt) // 如果給定位置過大,則默認返回最後元素
    {
        index = cnt - 1;
    }
    if(index < 0) // 如果給定位置過小,則默認返回首元素
    {
        index = 0;
    }
    int i = index;
    DNode<T> *ptr = head->next; // 指針指向首元素
    while(i--)
    {
        ptr = ptr->next;
    }
    return ptr;
}
template<class T>
DNode<T>* DList<T>::find(T elem) // 獲取elem第一次出現的位置的元素指針
{
    DNode<T> *ptr = head->next;
    while(ptr)
    {
        if(ptr->data == elem)
        {
            // 找到匹配元素,跳出循環,ptr指向該元素
            break;
        }
        ptr = ptr->next;
    }
    // 循環正常結束說明未找到,此時ptr==NULL
    return ptr;
}
template<class T>
int DList<T>::size() // 獲取鏈表的大小
{
    return cnt;
}
template<class T>
void DList<T>::each(char split, int method = 0) // 遍歷輸出鏈表,以split參數爲間隔,method爲0正向,爲1反向,默認0
{
    // 根據method初始化遍歷的起點
    DNode<T> *p = method ? tail : head->next;
    while(p &&  p != head) // 結束條件: p爲NULL,即到達鏈表尾;或者p等於head,即到達鏈表頭
    {
        cout<<(p->data);
        p = method ? p->previous : p->next; // 根據method決定遊標指針的移動方向
        putchar(p == NULL || p == head ? '\n' : split); // 輸出換行或分隔符
    }
}

int main()
{
    DList<int> lst;
    lst.push_front(1);
    lst.each(' ');
    lst.push_back(2);
    lst.each(' ');
    lst.push_front(0);
    lst.each(' ');
    lst.each(' ', 1);
    lst.del(1);
    lst.each(' ');
    lst.get(0)->data = 233;
    lst.find(2)->data = 666;
    lst.each(' ');

    return 0;
}
  附送一道雙向鏈表練習題,另外,單鏈表的題也可以用雙向鏈表做,傳送門:

  SDUT OJ 2054 數據結構實驗之鏈表九:雙向鏈表

  好啦,雙向鏈表就講完了,是不是跟單鏈表很像?確實很像,只要學會了單鏈表,雙向鏈表也不在話下,只要注意指針的細節處理就好。

  本章內容結束,歡迎繼續交流學習~

  下集預告&傳送門數據結構與算法專題之線性表——鏈表(三)循環鏈表

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章