如何使用C++實現單鏈表

線性表——鏈表

爲何假期也在發文章

//TODO NullGirlfrindExceptionios

請忽略以上兩行無聊的事實......算法

如何彌補順序表的不足之處?

第一次學習線性表必定會立刻接觸到一種叫作順序表(順序存儲結構),通過上一篇的分析順序表的優缺點是很顯然的,它雖然可以很快的訪問讀取元素,可是在解決如插入和刪除等操做的時候,卻須要移動大量的元素,效率較低,那麼是否有一種方法能夠改善或者解決這個問題呢?數組

首先咱們須要考慮,爲何順序表中的插入刪除操做會涉及到元素的移動呢?微信

好傢伙,問題就是圍繞着順序表的最大的特色出現的——順序存儲,相鄰放置元素,也就是說每一個元素都是根據編號一個一個挨着的,這就致使了 插入或刪除後,爲了仍然呈順序線性存儲,被操做元素後面的元素的位置均須要發生必定的變化,你應該能想象獲得,在擁擠的隊伍中忽然從中插入一個學生的場景,後面浩浩蕩蕩的人羣,口吐芬芳的向後挪了一個空位,若是人羣過大,從新排好隊也須要必定的時間函數

好嘛,人與人之間別這麼擠在一塊兒,每一個人與人之間都流出一點空隙來,留必定的位置出來,好了,這好像是個辦法,可是負責一個一個與學生交流填表的老師可就不幹了,這意味着我(找人)遍歷的時候,須要多跑好多路,浪費好多時間,先不說這個,體院館又不行了,大家這麼個擺法,我這小館可放不下,這也就意味着空間複雜度增長了不少學習

咱們剛纔所圍繞的都是在 "排隊" 的基本前提下的,但咱們能想到的方法並非很理想,那麼咱們索性就不排隊了,是否是能有更好的解決方式呢?ui

一個有效的方法:this

讓同窗們(元素)本身找位置隨便站,不過你要知道相對於本身下一位同窗的位置,這樣既解決了空間上的問題,又能經過這種兩兩聯繫的方式訪問(遍歷)到整個隊伍(數組),最重要的是,插入和離開同窗,因爲同窗(元素)之間不存在了那種排隊,相鄰的特色,因此也不會說影響到過多的同窗(元素)只須要和你插入位置的先後兩位同窗溝通好就好了,反正別人也不知道大家之間發生了什麼事spa

好了思路是有了,咱們來看一種最多見的鏈表——單鏈表設計

單鏈表的基本結構

這種鏈表爲何被稱做單鏈表呢?這是由於它只含有一個地址域,這是什麼意思呢?

咱們在鏈表中擯棄了順序表中那種一板一眼的排隊方式,可是咱們必須讓兩個應該相鄰的元素之間有必定的相互關係,因此咱們選擇讓每個元素能夠聯繫對應的下一個元素

而這個時候咱們就須要給每一個元素安排一個額外的位置,來存儲它的後繼元素的存儲地址,這個存儲元素信息的域叫作指針域或地址域,指針域中儲存的信息也叫做指針或者鏈

咱們用一張圖 看一下他的結構

結構中名詞解釋

  • 頭指針:一個指向第一個節點地址的指針變量
    • 頭指針具備標識單鏈表的做用,因此常常用頭指針表明單鏈表的名字
  • 頭結點:在單鏈表的第一個結點以前附設一個結點,它沒有直接前驅,稱之爲頭結點
    • 可不存信息,也能夠做爲監視哨,或用於存放線性表的長度等附加信息
    • 指針域中存放首元結點的地址
  • 首元結點:存儲第一個元素的節點

爲何要附設一個頭結點

咱們來解釋一下:

  • 鏈表若是爲空的狀況下,若是單鏈表沒有頭結點,那麼頭指針就會指向NULL,若是加上頭結點,不管單鏈表是否爲空,頭指針都會指向頭結點,這樣使得空鏈表與非空鏈表處理一致

  • 使首元結點前插入或刪除元素的時候,與後面操做相同,不須要產生額外的判斷分支,使得算法更加簡單

(以插入爲例講解)在帶頭結點的狀況下,在首元結點前插入或者刪除元素仍與在其餘位置的操做相同,只須要將前一個元素(在這裏是頭結點)的指針域指向插入元素,同時將插入元素的指針域指向原來的第二的元素

而無頭結點的狀況因爲,首元結點前沒有元素,只能經過修改head的先後關係,因此致使了 與在別的位置插入或刪除元素的操做不一樣,在實現這兩個功能的時候就須要額外的寫一個判斷語句來判斷插入的位置是否是首元結點以前的位置,增長了分支,代碼不夠簡潔

總結:頭結點的存在使得空鏈表與非空鏈表處理一致,也方便對鏈表首元結點前結點的插入或刪除操做

單鏈表的類型定義

線性表的抽象數據類型定義

咱們在給出單鏈表的定義以前咱們仍是須要先引入咱們線性表的抽象數據類型定義

#ifndef _LIST_H_
#define _LIST_H_
#include<iostream>
using namespace std;

class outOfRange{};
class badSize{};
template<class T>
class List {
public:
    // 清空線性表
    virtual void clear()=0;
    // 判空,表空返回true,非空返回false
    virtual bool empty()const=0;
    // 求線性表的長度
    virtual int size()const=0;
    // 在線性表中,位序爲i[0..n]的位置插入元素value
    virtual void insert(int i,const T &value)=0;
    // 在線性表中,位序爲i[0..n-1]的位置刪除元素
    virtual void remove(int i)=0;
    // 在線性表中,查找值爲value的元素第一次出現的位序
    virtual int search(const T&value)const=0;
    // 在線性表中,查找位序爲i的元素並返回其值
    virtual T visit(int i)const=0;
    // 遍歷線性表
    virtual void traverse()const=0;
    // 逆置線性表
    virtual void inverse()=0;                   
    virtual ~List(){};
};

/*自定義異常處理類*/ 


class outOfRange :public exception {  //用於檢查範圍的有效性
public:
    const char* what() const throw() {
        return "ERROR! OUT OF RANGE.\n";
    }
};

class badSize :public exception {   //用於檢查長度的有效性
public:
    const char* what() const throw() {
        return "ERROR! BAD SIZE.\n";
    }
};

#endif

單鏈表的類型定義

#ifndef _SEQLIST_H_
#define _SEQLIST_H_
#include "List.h"
#include<iostream>
using namespace std;

template<class elemType>
//elemType爲單鏈表存儲元素類型 
class linkList:public List<elemType> {
private:
    //節點類型定義 
    struct Node {
        //節點的數據域 
        elemType data;
        //節點的指針域 
        Node *next;
        //兩個構造函數 
        Node(const elemType value, Node *p = NULL) {
            data = value;
            next = p;
        } 
        Node(Node *p = NULL) {
            next = p;
        } 
    };
    
    //單鏈表的頭指針 
    Node *head;
    //單鏈表的尾指針 
    Node *tail;
    //單鏈表的當前長度 
    int curLength;
    //返回指向位序爲i的節點的指針 
    Node *getPostion(int i)const; 
public:
    linkList();
    ~linkList();
    //清空單鏈表,使其成爲空表 
    void clear();
    //帶頭結點的單鏈表,判空 
    bool empty()const {return head -> next == NULL;} 
    //返回單鏈表的當前實際長度
    int size()const {return curLength;}
    //在位序i處插入值爲value的節點表長增1 
    void insert(int i, const elemType &value); 
    //刪除位序爲i處的節點,表長減1
    int search(const elemType&value)const;
    //查找值爲value的節點的前驅的位序
    int prior(const elemType&value)const;
    //訪問位序爲i的節點的值,0定位到首元結點
    elemType visit(int i)const;
    //遍歷單鏈表
    void traverse()const;
    //頭插法建立單鏈表
    void headCreate();
    //尾插法建立單鏈表
    void tailCreate();
    //逆置單鏈表 
    void inverse();
};

單鏈表上的基本運算實現

(一) 單鏈表的初始化-構造函數

單鏈表的初始化就是建立一個帶頭節點空鏈表,咱們不須要設置其指針域,爲空便可

template<class elemType>
linkList<elemType>::linkList() {
    head = tail = new Node();
    curLength=0;
}

注意:new 操做符表明申請堆內存空間,上述代碼中應該判斷是否申請成功,爲簡單,默認爲申請成功,實際上若是系統沒有足夠的內存可供使用,那麼在申請內存的時候會報出一個 bad_alloc exception 異常

(二) 析構函數

當單鏈表對象脫離其做用域時,系統自動執行析構函數來釋放單鏈表空間,其實也就是清空單鏈表內容,同時釋放頭結點

template<class elemType>
linkList<elemType>::~linkList() {
    clear();
    delete head;
}

(三) 清空單鏈表

清空單鏈表的主要思想就是從頭結點開始逐步將後面節點釋放掉,可是咱們又不想輕易的修改頭指針head的指向,因此咱們引入一個工做指針,從頭結點一直移動到表尾,逐步釋放節點

template<class elemType>
void linkList<elemType>::clear() {
    Node *p, *tmp;
    p - head -> next;
    while(p != NULL) {
        tmp = p;
        p = p -> next();
        delete tmp; 
    }
    head -> next = NULL;
    tail = head;
    curLength = 0;  
}

(四) 求表長

因爲咱們的代碼中已經定義過一個叫作 curLength 的變量用來記錄咱們的表長

因此咱們能夠直接返回,咱們在定義中已經實現了,也就是這句

//返回單鏈表的當前實際長度
int size()const {return curLength;}

可是若是咱們沒有這樣一個變量,咱們想要實現這樣的功能又是什麼樣的方法呢?

template<class elemType>
int linkList<elemType>::size()const {
    Node *p = head -> next;
    int count;
    while(p) {count++; p = p -> next;}
    return count;
}

(五) 遍歷單鏈表

咱們須要從頭至尾訪問單鏈表中的每個節點,而且輸出其中數據域的信息

template<class elemType>
void linkList<elemType>::traverse()const {
    Node *p = head -> next;
    cout << "traverse:";
    while (p != NULL) {
        cout << p -> date << " ";
        p = p -> next;
    }
}

(六) 按照位序 i 尋找其元素對應內存地址

設置一個移動工做指針,和一個計數器 count,初始時p指向頭結點,每當指針p移向下一個結點的時候,計數器count + 1 ,直到 p指向位序爲 i的節點爲止。返回 p

template<class elemType>
typename linkList<elemType>::Node *linkList<elemType>::getPostion(int i)const {
    if(i < -1 || i > curLength - 1)
        return NULL;
    Node *p = head;
    int count = 0;
    while(count <= i) {
        p = p -> next;
        count++;
    }
    return p;
}

(七) 按值查詢節點位序

設置一個移動工做指針,和一個計數器 count,從單鏈表的第一個節點開始,開始於給定的值進行比對,若是相等則查找成功,返回節點的位序,不然繼續查詢知道單鏈表結束,查詢失敗返回 -1

template<class elemType>
int linkList<elemType>::search(const elemType&value)const {
    Node *p = head -> next;
    int count = 0; 
    while (p != NULL && p -> data != value) {
        p = p -> next;
        count++;
    }
    if (p == NULL) {
        return -1;
    }else {
        return count; 
    }
}

(八) 插入節點

在位序爲 i 出插入值爲value 的新節點q,咱們須要作的就是找到位序爲i - 1 的節點p,讓q指針域指向原來p的後繼,而後修改p的後繼爲q便可,說白了也就是修改插入元素位置先後的元素指向關係就能夠了

template<class elemType>
void linkList<elemType>::insert(int i,const elemType &value) {
    Node *p, *q;
    if(i < 0 || i > curLength)
        throw outOfRange();
    p = getPostion(i - 1);
    q = new Node(value,p -> next);
    p -> next = q;
    if (p == tail) tail = q;
    curLength++;
}

(九) 刪除節點

能看懂添加節點的方法,理解刪除節點也是手到擒來

template<class elemType>
void linkList<elemType>::remove(int i) {
    //p是待刪節點,pre是其前驅 
    Node *p, *pre;
    if(i < 0 || i > curLength)
        throw outOfRange();
    pre = getPostion(i - 1);
    p = pre -> next;
    if (p == tail) {
        tail = pre;
        pre -> next = NULL;
        delete p;
    } else {
        pre -> next = p -> next;
        delete p;
    }
}

單鏈表整表的建立

回顧咱們前面認識的順序表,它其實能夠理解爲一個數組,咱們聲明一個類型,同時給定值,初始化其大小,可是單鏈表就不同了,它是一種動態組織,它不須要像順序表同樣元素集中,它能夠隨着實際的狀況來動態生成節點,因此也不須要預先分配空間大小和位置

(一) 頭插法建立單鏈表

頭插法的意思就是說,每次新增節點所有插在頭結點以後,首元結點以前,你能夠這樣理解,我先來排隊,可是後面來了人,他就會排到個人前面去,咱們來藉助圖看一下

咱們一次插入元素 123 但實際上輸出的是按照321的順序存儲的,也就是說和咱們的邏輯順序是相反的

咱們來看一看怎麼實現它

template<class elemType>
void linkList<elemType>::headCreate() {
    Node *p;
    elemType value, flag;
    cout << "inputelements, ended with:";
    cin >> flag;
    while(cin >> value, value != flag) {
        //p -> data == value, p -> next = head ->next 
        p = new Node(value, head -> next);
        head -> next = p;
        //原鏈表爲空,新節點p成爲爲節點 
        if (head == tail) 
            tail = p;
        curLength++; 
    }
}

逆置單鏈表

咱們知道單鏈表中元素順序與讀入的順序是相反的,咱們能夠經過逆置單鏈表的算法,幫助咱們從新恢復咱們的慣有思惟順序

template<class elemType>
void linkList<elemType>::inverse() {
    Node *p, *tmp;
    //p爲工做指針,指向首元結點 
    p = head -> next;
    //頭結點的指針域置空,構成空鏈表 
    head -> next = NULL;
    //逆置後首元結點將成爲尾節點 
    if (p)
        tail = p;
    while (p) {
        //暫存p的後繼 
        tmp = p -> next;
        p -> next = head -> next;
        //節點p插在頭結點的後面 
        head -> next = p;
        //繼續處理下一個節點 
        p = tmp; 
    }
}

(二) 尾插法建立單鏈表

看完了頭插法,可是感受這樣的順序與咱們一向的思惟老是有一點彆扭,而尾插法則是一種,邏輯順序與咱們一致的建立方法

仍是看一下圖

template<class elemType>
void linkList<elemType>::tailCreate() {
    Node *p;
    elemType value, flag;
    cout << "inputelements, ended with:";
    cin >> flag;
    while(cin >> value, value != flag) {
        p = new Node(value,NULL);
        tail -> next = p;
        tail = p;
        curLength++;
    }
}

合併單鏈表

要求:假設咱們給出兩個仍然是遞增的單鏈表la和lb,咱們將其合併爲lc 仍保證遞增,利用原表空間,可是咱們仍在下面將表C稱做新表

由於咱們的要求是遞增的,因此使用尾插法是很是合適的,咱們設計三個工做指針,分別指向兩個表的首元結點,而後將第三個指針指向新表的頭結點,比較前兩個指針指向的值,小的就放到新表的表尾,而後後移動兩表中較小的那一個的指針,以此類推,直到其中一個表尾空,將剩餘的節點所有連接到新表的末尾

template<class elemType>
typename linkList<elemType> *linkList<elemType> ::Union(linkList<elemType> *lb) {
    Node *pa, *pb, *pc;
    linkList<elemType> *lc = this;
    pa = head -> next;
    head -> next = NULL;
    pb = (lb -> head) -> next;
    (lb -> head) -> next = NULL;
    
    pc = lc -> head;
    while(pa && pb) {
        if(pa -> data <= pb -> data) {
            pc-> next = pa;
            pc = pa;
            pa = pa -> next;
        } else {
            pc -> next = pb;
            pc = pb;
            pb = pb -> next;
        }
    }
    if(pa) {
        pc -> next = pa;
        lc -> tail = tail;
    } else {
        pc -> next = pb;
        lc -> tail = lb -> tail;
    }
    lc -> cuirLength = curLength + lb -> curLength;
    delete lb;
    return lc; 
}

總結

單鏈表,採起了鏈式存儲結構,用一組任意的存儲單元存放線性表的元素,尤爲對於須要頻繁的插入和刪除數據的時候更加適用,若是須要進行頻繁的查找仍是推薦使用順序表,例如對於一個學生成績管理系統的製做,學生更多的時候是查看本身的成績,而錄入的老師,也只有在考試後錄入一次,因此應該使用順序表,而例如考勤打卡系統,更多的是打卡信息的記錄,因此仍是選擇使用鏈表,固然例子可能不是很恰當,同時正常的開發中還會有更多複雜的問題須要考慮,舉例子只爲了利於理解

結尾:

若是文章中有什麼不足,或者錯誤的地方,歡迎你們留言分享想法,感謝朋友們的支持!

若是能幫到你的話,那就來關注我吧!若是您更喜歡微信文章的閱讀方式,能夠關注個人公衆號

在這裏的咱們素不相識,卻都在爲了本身的夢而努力 ❤

一個堅持推送原創開發技術文章的公衆號:理想二旬不止

相關文章
相關標籤/搜索