鏈表的相關操作

在學習數據存儲的時候,我們就曾經遇到過兩種存取方式,第一種是使用連續的數組來存放數據,還有一種便是使用鏈表的方式來進行存儲。
一般來說,使用數組來存放數據的時候我們常常會遇到的問題就是以下:
  • 存儲空間需要預先指定,這樣一來就對那些本來不是很明確需要多少存儲空間的數據帶來了一定的麻煩,如果我們在指定的時候分配了較少的空間,則相當有可能導致數組越界的問題的出現;而當我們穩妥起見分配了較多空間的時候,事實上又是一種浪費;
  • 數組的插入比較單一,只能選擇將數據插在數組的尾部;
  • 最爲致命的問題就是數組在處理刪除問題上,將會相當麻煩!我們想要刪除一個數據,如果這個數據本來是在數組的最後,那麼這種情況還能夠讓人接受,直接刪除該數據就好了;但是如果我們想要刪除的數據在數組中,那麼問題就來了,我們不僅要刪除而且,還要將待刪除後面的數據一個個向前移動,這樣的效果是極其差的!
  • ……

一般來說鑑於以上的種種原因,我們更大程度上願意使用鏈表這一數據結構來完成對數據的存儲,鏈表是將每個帶存放的數據封裝在一個結點內,該結點的實現大致如下:

/**
* 定義一個結點類,其中data用來存放待存放的數據,其類型可以是我們想要存放的各種類型
* 而next指針則是用來指向下一個結點的指針
*/
class Node
{
public:
     Node(int x){
        data = x;
        next = nullptr;
     }
    ~ Node();
    int getData();
    Node* getNext();
    void setData(int x);
    void setNext(Node* node);
private:
    Node *next;
    int data;
};
由以上的Node類的定義我們可以明白,在鏈表中我們數據的存放大致是怎樣進行的
——由data負責放置數據,而next則是用於指向下一個結點的指針。
這就告訴我們鏈表的存儲將不會是像數組那樣需要分配連續的存儲空間來存放數據,相反,鏈表的空間是不連續的,正是由於這個特性,我們在處理在特定的數據(結點)後插入新結點,刪除某一結點就變得相對容易很多。
我們先來看看鏈表類,對於一個鏈表我們要實現的操作主要有:
  • 插入(insert)
  • 刪除(remove)
  • 查詢(find)
  • 長度(size)
  • 遍歷(traversal)
  • 排序(order)
  • 全部刪除(deleteAll)
/**
* 鏈表的類定義
* 私有成員的中的兩個指針,其中root就是整個鏈表的頭結點,
* 而len則是整個鏈表的長度
*/
class List
{
public:
    List(){
        root = nullptr;
        len = 0;
    }
    ~List(){
        delete root;
    }
    int size();
    Node* insert(int x);
    bool remove(int x);
    Node* find(int x);
    bool deleteAll();
    void order();
    void traversal();
    void setHead(Node* node);
private:
    Node* root;
    Node* head;
    int len;
};
我們看以上的鏈表類類定義,可能大家發現了我在私有成員定義的時候定義了兩個頭結點指針——head和root,那麼,這兩個都是頭結點,究竟有什麼區別呢?
事實上,由於我在這裏使用的是尾插法向鏈表中插入數據,需要時時刻刻記錄鏈表的頭指針,但是尾插法在使用的時候除了頭指針外還需要兩個指針迭代插入。因此我將root設置爲在插入過程中慢慢的跟着鏈表動;而head也是整個鏈表的頭結點,head和root的唯一區別就是,head始終指向鏈表頭,而root會隨着插入而不斷移動。
接下來我們來看看鏈表的插入操作:
/**
* 鏈表類的插入函數
* 在插入的同時完成對鏈表長度的計數
*/
Node* List::insert(int x){
    Node* node = new Node(x);
    if (root == nullptr)
    {
        root = node;
    }else{
        root -> next = node;
        root = node;
    }

    ++ len;
    return root;
}
在鏈表的插入函數中,我們不必考慮越界問題(除非計算機的內存全部用完,不過這種情況還是相當罕見的),我們需要考慮的問題主要有兩點:
  1. 當我們剛new一個新的鏈表對象的時候,裏面的數據爲空,頭結點也爲空,這個時候的插入應當是怎樣的?
  2. 當我們的頭結點非空的時候,我們有應當怎樣插入結點?
對於第一個問題,因爲頭結點爲空嘛,所以我們直接將插入的數據使用構造函數封裝成爲一個結點對象,直接賦值給頭節點就好了。
對於第二個問題,我們採用的就是以下的方法來進行插入的操作,

list&

    (在這裏我要說我原來犯過的一個錯誤,一個bug。在剛開始我在寫Node結點類的時候,爲了安全起見,我是將data和next兩個變量設置爲私有成員的,就像以下的情況:
// version 1.0
class Node
{
public:
     Node(int x){
        data = x;
        next = nullptr;
     }
    ~ Node();
    int getData();
    Node* getNext();
private:
    Node *next;
    int data;
};
    然後,正當我興高采烈的時候,相當興奮的先編譯一波,結果卻得到以下的報錯:  expression is not assignable!!
D:\Program Files\LLVM>clang++ C:\Users\50萌主\Desktop\demo.cpp -o     C:\Users\50萌主\Desktop\demo.exe
     C:\Users\50钀屼富\Desktop\demo.cpp:68:21: error: expression is not assignable
                root -> getNext() = node;
                ~~~~~~~~~~~~~~~~~ ^
C:\Users\50钀屼富\Desktop\demo.cpp:82:21: error: expression is not assignable
                head -> getNext() = nullptr;
                ~~~~~~~~~~~~~~~~~ ^
C:\Users\50钀屼富\Desktop\demo.cpp:124:26: error: expression is not assignable
                        previous -> getNext() = node -> getNext();
                        ~~~~~~~~~~~~~~~~~~~~~ ^
C:\Users\50钀屼富\Desktop\demo.cpp:148:23: error: expression is not assignable
                                temp -> getData() = tem -> getData();
                                ~~~~~~~~~~~~~~~~~ ^
C:\Users\50钀屼富\Desktop\demo.cpp:149:22: error: expression is not assignable
                                tem -> getData() = i;
                                ~~~~~~~~~~~~~~~~ ^
5 errors generated.
   就很難受,明明我在使用結構體來定義Node的時候就啥事兒都沒有啊???!!!!!
   後來官網查詢之後發現這個錯誤是因爲,錯誤地對“右值”進行賦值操作!我當時就相當想的不明白,其他的方法也不能解決,後來索性直接將這兩個變量的權限設置爲public,並刪除了相應的get方法,也就是以下的代碼       
// version 1.1
class Node
{
public:
     Node(int x){
        data = x;
        next = nullptr;
     }
    ~ Node();
    Node *next;
    int data;
};
    後來越來越覺得設置爲public權限確實不安全,但還是想不明白,於是又一次索性去看了看Java,這一個不小心就看到Java中的類的定義,有變量,有get方法,還有對應的set方法!!!什麼?set方法!!set方法!!
    腦海突然靈光一現,咱們原來賦值不是通過set方法來進行的麼???什麼時候對get方法獲取的值賦值過啊。這下全部明白了,原來get方法返回的值是一個“右值”,因此不能被用於賦值。原來鬧了半天的bug原來是自己太不小心導致的……
    於是就有了第三個版本,這次很安全的版本:
// version 1.2
class Node
{
public:
     Node(int x){
        data = x;
        next = nullptr;
     }
    ~ Node();
    int getData();
    Node* getNext();
    void setData(int x);
    void setNext(Node* node);
private:
    Node *next;
    int data;
};

)

然後,我們來看看怎樣實現鏈表中的尋找,也即查詢方法——
主要的思路就是從頭結點開始,然後按照鏈表的指針指向順序開始遍歷,當找到第一個與待查找的數據的值相等的時候,就跳出遍歷,同時返回包含這個數值的結點,若全部遍歷完仍然沒找到待查找數據,則將返回一個空指針。
以下是代碼實現:
/**
*  查找函數(準確來說,是查詢鏈表中第一個)
*  返回含有待查值的結點
*/
Node* List::find(int x){
    Node* node = new Node(x);
    Node* temp = head;
    while(temp != nullptr){
        if (temp -> getData() == x){
                return temp;
        }else{
            temp = temp -> getNext();
            continue;
        }
    }

    return nullptr;
}
接下來,我們就來看看數據的刪除是怎樣實現的。
/**
* 刪除函數,首先需要找到待刪除數據的結點,
* 找到結點之後,分成兩種情況,其中一種是刪除頭結點的情況
* 還有一種是刪除非頭結點的情況
* 同時在刪除成功的時候,將鏈表的長度減少
*/
bool List::remove(int x){
    Node* node = find(x);
    if (node == nullptr)
        return false;
    else{
        /*if the node that you want to remove is the root node*/
        if (head -> getData() == x){
            head = head -> getNext();
        }else{
            /* find the aim target's previous node */
            Node* temp = head;
            Node* previous;
            while(temp != nullptr){
                if ((temp -> getNext()) -> getData() == x){
                    previous = temp;
                    break;
                }
                temp = temp -> getNext();
            }
            // previous -> getNext() = node -> getNext();
            previous -> setNext(node -> getNext());
        }
    }
    -- len;
    return true;
}
對於以上的的兩種情況,
  1. 當我們查找之後發現待刪除的是頭結點的時候,這樣就比較簡單,在這種情況下,我們直接將頭結點的指針向後移動一位就可以了;
  2. 當我們待刪除的是非頭結點的時候,這時就需要我們先找到待刪除結點的前一個指針(在這裏我們只能通過從頭到尾的遍歷,才能找到他的前驅。但是如果我們採用的是雙向鏈表的話將會很方便的找到前驅),然後再按照以下圖示的方法來完成刪除操作。
    list remove
完成了以上的這些對單個結點的操作之後,我們開始全局對整個鏈表進行操作:
首先我們開始看看遍歷操作:
遍歷操作比較簡單,主要的難點集中在確定整個鏈表的頭結點上,想必大家還記得我在定義鏈表屬性的時候,有定義過一個head和root兩個頭結點,而head就是我們遍歷時所需要的,因此我們要想辦法使得head指針的指向就是鏈表的頭結點,怎樣實現呢?
我們當時將insert方法的返回類型,設置爲Node*就是爲了給head賦值;
根據我們當時的插結點算法,我們不難知道當我們剛創建一個新的鏈表對象,並且插入第一個值之後返回的就是頭結點,所以,我們使用setHead方法來實現對head的賦值(這次我們使用的是set函數來實現的,沒有對get方法直接賦值)——
List* list = new List();
list -> setHead(list -> insert(5));
獲取了head頭結點之後,現在我們就可以開始遍歷了:
// 遍歷函數
void List::traversal(){
    Node* temp = head;
    while(temp != nullptr){
        cout << temp -> getData() << " ";
        temp = temp -> getNext();
    }
}
細心的小夥伴應當發現了,我在遍歷的時候並沒有直接使用head作爲頭結點進行直接遍歷,反而是創建了一個臨時對象,使用的是這個臨時對象來進行遍歷。原因是什麼呢?
是因爲如果我們直接使用head進行遍歷的話,那麼事實是遍歷完一遍之後,head就直接指向最後一個了。因此我們需要新建一個臨時對象來實現遍歷操作。
好,接下來我們就做一件相當刺激的事兒,完成這個相當刺激的功能,那就是刪除全部元素。
這一功能比較簡單,總的來說就是讓head的next指針指向nullptr,然後再讓head本身指向nullptr。
// 刪除所有
bool List::deleteAll(){
    if (head == nullptr)
    {
        return false;
    }else{
        // head -> getNext() = nullptr;
        head -> setNext(nullptr);
        head = nullptr;
    }
    len = 0;
    return true;
}
最後我們來實現一個排序的功能,這個函數只能用於那些能夠使用>、<符號比較大小的數據類型上,對於自定義的數據需要重載運算符才能使用,這裏我們不能使用快排,在這裏我們使用冒泡排序來實現鏈表的排序,代碼如下:
/*Bubble Sort*/
void List::order(){
    Node* temp = head;
    while (temp -> getNext() != nullptr){
        for (auto tem = temp -> getNext(); tem != nullptr ; tem = tem -> getNext()){
            if (temp -> getData() > tem -> getData()){
                int i = temp -> getData();
                // temp -> getData() = tem -> getData();
                temp -> setData(tem -> getData());
                // tem -> getData() = i;
                tem -> setData(i);
            }
        }
        temp = temp -> getNext();
    }
}
這就大致完成了整個鏈表的大致全部功能。
以上。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章