數(shù)據(jù)結構之LinkList鏈表

鏈表與數(shù)組在數(shù)據(jù)結構的江湖上被并稱為南數(shù)組、北鏈表,其江湖地位可見一斑

概念

鏈表作為最基礎的通用存儲結構,它的作用和數(shù)組是一樣的,但存儲數(shù)據(jù)的方式略有不同。數(shù)組需要預先獲取一定的內存空間,且內存的地址是連續(xù)的,在存儲數(shù)據(jù)的時候,插入和刪除都需要遍歷數(shù)組,還需要移動部分數(shù)據(jù),是十分低效的。而鏈表能夠解決以上問題,下面來看一下鏈表的結構。

鏈表的數(shù)據(jù)項存儲在鏈表節(jié)點中,一個節(jié)點可以叫Node也可以叫Link,它包含數(shù)據(jù)項和一個引用,數(shù)據(jù)項存儲數(shù)據(jù)本身,通常是一個包含各種數(shù)據(jù)的類的對象,而引用則是引用下一個節(jié)點,一般記為next,鏈表的核心實現(xiàn)就是這個next,它標記了每個節(jié)點之間的聯(lián)系,像一根線一樣把這些散落在內存各個角落的節(jié)點對象串了起來。下面通過代碼來直觀的理解一下:

/**
 * 鏈表的節(jié)點,用來存放數(shù)據(jù)
 */
public class Node {
    // 數(shù)據(jù)變量
    private String data;
    // next引用
    public Node next;
    // 構造函數(shù)
    public Node(String str) {
        data = str;
    }
    // getter
    public String getData() {
        return data;
    }
    // Node的打印方法
    public void displayNode() {
    System.out.print("{ Node: " + data + " }..");
    }
}

可以看到,鏈表節(jié)點的關鍵兩個屬性,一個是數(shù)據(jù)項(這里簡單的表示為String類型的data),一個是指向下一個鏈表節(jié)點的next引用(這里為了訪問簡單設置為public),雖然還不知道下一個節(jié)點是誰,但在鏈表的數(shù)據(jù)數(shù)據(jù)結構中,將數(shù)據(jù)插入時,就會建立這個聯(lián)系。

單向鏈表

以上說明了鏈表的重要概念,節(jié)點與引用,下面來展示如何構成一個鏈表,首先從最簡單的單向鏈表開始,單向鏈表顧名思義,只有一端可以插入和訪問的鏈表,鏈表類LinkList值僅維護第一個鏈表節(jié)點first的信息,其它的節(jié)點通過next引用,以下是實現(xiàn)代碼:

/**
 * 單向鏈表:維護頭節(jié)點的信息,對鏈表的操作都從頭部開始
 */
public class LinkList {
    // 頭節(jié)點的引用
    Node first;
    // 構造方法
    public LinkList() {
        first = null;
    }
    // 插入方法,頭插法
    public void insertFirst(String str) {
        Node newNode = new Node(str);
        newNode.next = first;
        first = newNode;
    }
    // 刪除方法,刪除頭部節(jié)點
    public Node deleteFirst() {
        Node temp = first;
        first = first.next;
        return temp;
    }
    // 打印鏈表方法
    public void display() {
        System.out.print("LinkList: ");
        Node current = first;
        while (current != null) {
            current.displayNode();
            current = current.next;
        }
        System.out.println();
    }
    // 判斷鏈表是否為空的方法
    public Boolean isEmpty() {
        return first == null;
    }
    // 在鏈表中查找的方法
    public Node find(String str) {
        Node current = first;
        while (current != null) {
            if (current.getData() != str)
                current = current.next;
            return current;
        }
        System.out.println("could not find Node :" + str);
        return null;
    }
    // 從鏈表中查找元素刪除方法
    public Node delete(String str) {
        Node current = first;
        Node previous = first;
        while (current != null) {
            if (current.getData() != str) {
                previous = current;
                current = current.next;
            } else {
                previous.next = current.next;
                return current;
            }
        }
        System.out.println("cloud not find Node:" + str + ", failed to delete it");
        return null;
    }
}

代碼中定義了唯一一個變量:頭節(jié)點的引用first,還有一些鏈表的常用方法:插入、刪除、查找、展示等,可以發(fā)現(xiàn)這些方法都是由頭節(jié)點first展開的,在表達這些方法的時候需要注意邊界和特殊情況first的分析。

雙端鏈表

注意是雙端列表,不是雙向鏈表,在上面的單向鏈表中,鏈表類僅維護了一個頭節(jié)點的引用first,那么如果要在鏈表尾部插入一個數(shù)據(jù),需要遍歷整個鏈表,這種效率很低。雙端鏈表不僅維護了first頭節(jié)點的引用,而且還維護了last尾節(jié)點的引用,這樣訪問尾節(jié)點就像訪問頭節(jié)點一樣方便,這種特性使得雙端鏈表比起單向鏈表在一些場合更有效率,比如說隊列。

以下展示雙端鏈表的實現(xiàn)代碼,節(jié)點還是引用上面的Node類

public class DoubleSideLinkList {

    // 首節(jié)點和尾節(jié)點的引用
    Node first;
    Node last;

    // constructor
    public DoubleSideLinkList() {
        first = null;
        last = null;
    }

    // isEmpty
    public boolean isEmpty() {
        return first == null;
    }

    // insert first
    public void insertFirst(String str) {
        Node newNode = new Node(str);
        if (isEmpty()) {
            last = newNode;
        }
        newNode.next = first;
        first = newNode;
    }

    // insert last
    public void insertLast(String str) {
        Node newNode = new Node(str);
        if (isEmpty()) {
            first = newNode;
        }
        last.next = newNode;
        last = newNode;
    }

    // delete first
    public Node deleteFirst() {
        Node temp = first;
        // 僅有一個元素
        if (first.next == null)
            last = null;
        first = first.next;
        return temp;
    }

    // display
    public void displayList() {
        System.out.println("List (first-->last): ");
        Node current = first;
        while(current != null) {
            current.displayNode();
            current = current.next;
        }
        System.out.println();
    }
}

可以看出,雙端鏈表可以很方便的從尾節(jié)點插入數(shù)據(jù),這里需要說明的是,這個鏈表依然是單向的,每個節(jié)點也僅有一個引用next指向下一個節(jié)點,所以遍歷和查找的方法與單向鏈表沒什么差別,在此就不做展示。在實現(xiàn)雙端鏈表的insert與delete方法的時候需要注意特殊情況:列表為空或者列表將要為空時,first與last的處理。

有序鏈表

有序鏈表中,數(shù)據(jù)是按照關鍵值的有序排列的,有序鏈表的插入速度優(yōu)于有序數(shù)組,而且可以擴展到所有有效的內存,所以在很多場合可以使用有序鏈表,在以下的實現(xiàn)中,我們還是引用上面的Node類,因為數(shù)據(jù)項data為String類型,我們以數(shù)據(jù)項的hashcode為序來實現(xiàn)有序鏈表:

public class SortedLinkList {

    // filed
    Node first;

    // constructor
    public SortedLinkList() {
        first = null;
    }

    public boolean isEmpty() {
        return (first==null);
    }

    public void insert(String key) {
        Node current = first;
        Node previous = null;
        Node newNode = new Node(key);

        while (current != null && current.getData().hashCode() < key.hashCode()) {
            previous = current;
            current = current.next;
        }
        if (previous == null) {
            first = newNode;
        } else {
            previous.next = newNode;
        }
        newNode.next = current;
    }

    public Node remove() {
        Node temp = first;
        first = first.next;
        return temp;
    }

    public void display() {
        System.out.print("LinkList: ");
        Node current = first;
        while (current != null) {
            current.displayNode();
            current = current.next;
        }
        System.out.println();
    }
}

有序鏈表主要是insert方法比較難實現(xiàn),也是需要考慮特殊情況,即鏈表為空。

雙向鏈表

雙向鏈表與雙端鏈表不同,雙端鏈表雖然可以從尾部插入節(jié)點,但是每個節(jié)點引用的還是其下一個節(jié)點,遍歷也是單向的,而雙向鏈表則可以從頭部或者從尾部開始遍歷,為了實現(xiàn)這一特性,我們需要修改一下節(jié)點類Node,為了區(qū)分,我們以Link為節(jié)點命名,在Link中,不僅實現(xiàn)對下一節(jié)點的引用next,而且還實現(xiàn)了對上一個節(jié)點的引用previous,如下:

public class Link {

    private int data;

    public Link next;

    public Link previous;

    public Link(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }

    public void displayLink() {
        System.out.print(data + " ");
    }
}

唯一不同的點就是指向上一個節(jié)點的引用previous,雖然僅在節(jié)點中加了一個引用,但是雙向鏈表的實現(xiàn)卻麻煩了很多,其中各種引用關系特別容易混亂,而且還有一些特殊情況需要考慮,下面一起來感受一下:

public class DoublyLinkList {

    // 頭節(jié)點和尾節(jié)點的引用,也是雙端的
    Link first;
    Link last;

    // constructor
    public DoublyLinkList() {
        first = null;
        last = null;
    }

    // isEmpty
    public boolean isEmpty() {
        return first == null;
    }

    // insert first
    public void insertFirst(int data) {
        Link newLink = new Link(data);

        if (isEmpty()) {
            last = newLink;
        } else {
            first.previous = newLink;
        }
        newLink.next = first;
        first = newLink;
    }

    // insert last
    public void insertLast(int data) {
        Link newLink = new Link(data);

        if (isEmpty()) {
            first = newLink;
        } else {
            last.next = newLink;
            newLink.previous = last;
        }
        last = newLink;
    }

    // insert after
    public boolean insertAfter(int key, int data) {
        Link newLink = new Link(data);
        Link current = first;
        // find
        while(current != null) {
            if (current.getData() != key) {
                current = current.next;
            } else {
                if (current == last) {
                    newLink.next = null;
                    last = newLink;
                } else {
                    newLink.next = current.next;
                    current.next.previous = newLink;
                }
                current.next = newLink;
                newLink.previous = current;
                return true;
            }
        }
        System.out.println("cloud not find the key: " + data);
        return false;
    }

    // delete first
    public Link deleteFirst() {
        Link temp = first;
        if (first.next == null) {
            last = null;
        } else {
            first.next.previous = null;
        }
        first = first.next;
        return temp;
    }

    // delete last
    public Link deleteLast() {
        Link temp = last;
        if (first.next == null) {
            first = null;
        } else {
            last.previous.next = null;
        }
        last = last.previous;
        return temp;
    }

    // delete key
    public Link deleteKey(int data) {
        Link current = first;
        while (current != null) {
            if (current.getData() != data) {
                current = current.next;
            } else {
                if (current == first) {
                    first = first.next;
                } else {
                    current.previous.next = current.next;
                }
                if (current == last) {
                    last = last.previous;
                } else {
                    current.next.previous = current.previous;
                }
                return current;
            }
        }
        System.out.println("cloud not find the key: " + data + ", delete failed!");
        return null;
    }

    // display forward
    public void displayForward() {
        System.out.println("List (first-->last): ");
        Link current = first;
        while (current != null) {
            current.displayLink();
            current = current.next;
        }
        System.out.println();
    }

    // display backward
    public void displayBackward() {
        System.out.println("List (last-->first): ");
        Link current = last;
        while (current != null) {
            current.displayLink();
            current = current.previous;
        }
        System.out.println();
    }
}

這是一個雙向鏈表,同時也是雙端列表,可以從前向后或者從后向前遍歷,也可以在鏈表的任何地方插入或是刪除節(jié)點,在實現(xiàn)這些方法時,需要修改與插入或刪除節(jié)點有關系的前后四個引用,且還需要考慮first與last、列表為空或者僅有一個元素的特殊情況,可以通過畫圖來加深理解。

效率與總結

以上我們介紹并實現(xiàn)了單向鏈表、雙端鏈表、有序鏈表和雙向鏈表,接下來我們來討論一下這些鏈表的效率。
鏈表與數(shù)組作為通用數(shù)據(jù)結構經(jīng)常被拿來做對比,以鏈表和數(shù)組這兩個通用數(shù)據(jù)結構為基礎的一些抽象數(shù)據(jù)模型,比如java中的ArrayList與LinkedList,也經(jīng)常被拿出來作比較,總的來說主要有兩大區(qū)別:

  1. 鏈表插入和刪除的效率高于數(shù)組,鏈表插入、刪除頭節(jié)點、尾節(jié)點的時間效率為O(1),而在鏈表中間插入、刪某一節(jié)點的時間復雜度為O(N),雖然與數(shù)組相同操作的時間復雜度相同,可是數(shù)組需要復制移動元素位置來填補空洞,所以鏈表的效率還是要優(yōu)于數(shù)組的,特別是復制時間占比較重的情況中。
    數(shù)組則是隨機訪問元素的效率要高于鏈表,可以通過數(shù)組下標直接訪問的到。

  2. 鏈表比數(shù)組優(yōu)越的另一個重要因素就是,鏈表可以擴展到所有可用的內存空間,且不用像數(shù)組一樣初始化容量,也不需要可以擴容。另外當一個數(shù)組對象需要一個連續(xù)的大內存空間時,會有幾率觸發(fā)jvm的GC去整理內存空間以獲取一片連續(xù)的內存,而鏈表則沒有這方面的限制,所以在內存的使用效率上,鏈表優(yōu)于數(shù)組。

正如以上所述,一般來說,鏈表如果采用頭插法(或刪除),則時間復雜度均為O(1),而如果是隨機插入或者向有序鏈表中插入的時間效率均為O(N),因為需要找到合適的位置;

有序鏈表可以在O(1)的時間返回或刪除最小或最大值,如果一個應用需要頻繁的返回最小值且不需要快速的插入時間,則可以選擇有序鏈表來實現(xiàn),如優(yōu)先級隊列;

雙向鏈表由于兩端都可以插入、刪除和遍歷,所以可以用來做雙端隊列。

參考文章

《Data Structures & Algorithms in Java》 Robert Lafore著

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • 一些概念 數(shù)據(jù)結構就是研究數(shù)據(jù)的邏輯結構和物理結構以及它們之間相互關系,并對這種結構定義相應的運算,而且確保經(jīng)過這...
    Winterfell_Z閱讀 5,879評論 0 13
  • ? 數(shù)據(jù)結構 數(shù)據(jù)結構是計算機存儲和組織數(shù)據(jù)的的方式 1. 數(shù)組 在Java中,數(shù)組是用來存放同一種數(shù)據(jù)類型的集合...
    欲火逢生閱讀 519評論 0 3
  • 前面博客我們在講解數(shù)組中,知道數(shù)組作為數(shù)據(jù)存儲結構有一定的缺陷。在無序數(shù)組中,搜索性能差,在有序數(shù)組中,插入效率又...
    IT可樂閱讀 613評論 0 2
  • 一、鏈表的定義 鏈表是一種遞歸的數(shù)據(jù)結構,是一種線性結構,但是并不會按線性的順序存儲數(shù)據(jù),而是在每一個節(jié)點里存到下...
    熊喵先森閱讀 1,484評論 0 3
  • (上)如何實現(xiàn)LRU緩存淘汰算法? 一、什么是鏈表? 1.和數(shù)組一樣,鏈表也是一種線性表。2.從內存結構來看,鏈表...
    碼語生活閱讀 320評論 0 0