Java數據結構與算法(第五章鏈表)

        數組做爲數據存儲機構有必定的缺陷。在無序數組中,搜索是低效的;而在有序數組中,插入效率又很低;無論在哪種數組重刪除效率都很低。何況一個數組建立後,它的大小是不可改變的。java

        鏈表多是繼數組以後第二種使用得最普遍的通用存儲結構。
redis

鏈結點(Link)算法

        在鏈表中,每一個數據項都被包含在「鏈結點」(Link)中。一個鏈結點是某個類的對象,這個類能夠叫作Link。由於一個鏈表中有許多相似的鏈結點,因此有必要用一個不一樣於鏈表來表達鏈結點。每一個Link對象中都包含一個對下一個鏈結點引用的字段(一般叫作next)。可是鏈表自己的對象有一個字段指向對第一個鏈結點的引用。
編程

public class Link {
    public int iData;
    public double dData;
    public Link next;
}

引用和基本類型數組

        在鏈表的環境中,很容易對「引用」產生混淆。
數據結構

        在Link的類定義中定義了一個Link類型的域,這看起來很奇怪。編譯器怎樣才能不混淆呢?編譯器在不知道一個LInk對象佔多大空間的狀況下,如何能知道一個包含了相同對象的Link對象佔用多大空間呢?
dom

        在Java語言中,這個wen ti的da an是Link對象並無真正包含另一個Link對象。看似包含了,類型的Link的next字段僅僅是對另一個Link對象的「引用」,而不是一個對象。
函數

關係而不是位置工具

        鏈表不一樣於數組的主要特性之一。在一個數組中,每一項佔用一個特定的位置。這個位置能夠用一個下標號直接訪問。它就像一排房子,你能夠憑地址找到其中特定的一間。
編碼

        在鏈表中,尋找一個特定的元素的惟一方法就醫沿着這個元素的鏈一直向下找。它很想人們之間的關係。可能你問Harry,Bob在哪兒,Harry不知道,可是他想Jane可能知道,因此你又去問Jane。Jane看到Bob和Sally一塊兒離開了公司,因此你打Sally的手機,她說她在Peter的辦公室和Bob分開了,因此。。。。。。可是總有線索,不能直接訪問到數據項;必須使用數據之間的關係來定位它。從第一項開始,到第二項,而後到第三個,知道發現要找的那個數據項。

單鏈表

雙端鏈表

鏈表的效率

        在表頭插入和刪除速度很快。僅須要改變一兩個引用值,因此花費O(1)的時間。

        平均起來,查找、刪除和在指定鏈結點後面插入都須要搜索鏈表中的一半鏈結點。須要O(N)次比較。在數組中執行這些操做也須要O(N)次比較,可是鏈表仍然要快一些,由於當插入和刪除鏈結點時,鏈表不須要移動任何東西。增長的效率是很顯著的,特別是當複製時間遠遠大於比較時間的時候。

        鏈表比數組優越的另一個重要方面是鏈表須要多少內存就能夠用多少內存,而且能夠擴展到全部可用內存。數組的大小在它建立的時候就固定了;因此常常有序數組太大致使效率低下,或者數組過小致使空間溢出。向量是一種可擴展的數組,它能夠經過變長度解決這個問題,可是它常常只容許以固定大小的增量擴展(例如快要溢出的時候,就增長一倍數組容量)。這個解決方案在內存使用效率上來講仍是要比鏈表的低。

抽象數據類型(ADT)

        抽象數據類型(ADT)。簡單說來,它是一種kao慮數據結構的方式:着重於它作了什麼,而忽略它是作麼作的。

        棧和隊列都是ADT的例子。前面已經看到棧和隊列均可以用數組來實現。在繼續ADT的討論以前,先看一下如何用鏈表實現棧和隊列。這個討論將展現棧和隊列的「抽象」特性:即如何脫離具體實現來kao慮棧和隊列。

用鏈表實現棧Java代碼:

package com.stack.linkstack;
public class LinkStack {
    private LinkList theList;
    public LinkStack(){
        theList = new LinkList();
    }
    public void push(long j){
        theList.insertFirst(j);
    }
    public long pop(){
        return theList.deleteFirst();
    }
    public boolean isEmpty(){
        return theList.isEmpty();
    }
    public void displayStack(){
        System.out.print("Stack (top --> bottom): ");
        theList.displayList();
    }
}
class Link{
    public long dData;
    public Link next;
    public Link(long dd){
        dData = dd;
    }
    public void displayLink(){
        System.out.print(dData+" ");
    }
}
class LinkList{
    private Link first;
    public LinkList(){
        first = null;
    }
    public boolean isEmpty(){
    return first == null;
    }
    public void insertFirst(long dd){
        Link newLink = new Link(dd);
        newLink.next = first;
        first = newLink;
    }
    public long deleteFirst(){
        Link temp = first;
        first = first.next;
        return temp.dData;
    }
    public void displayList(){
        Link current = first;
        while(current!=null){
            current.displayLink();
            current = current.next;
        }
        System.out.println("");
    }
}
public class LinkStackApp {
    public static void main(String[] args) {
        LinkStack theStack = new LinkStack();
        
        theStack.push(20);
        theStack.push(40);
        
        theStack.displayStack();
        
        theStack.push(60);
        theStack.push(80);
        
        theStack.displayStack();
        theStack.pop();
        theStack.pop();
        
        theStack.displayStack();
    }
}

//輸出:
Stack (top --> bottom): 40 20 
Stack (top --> bottom): 80 60 40 20 
Stack (top --> bottom): 40 20


用鏈表實現隊列Java代碼:

package com.queue.linkqueue;
public class LinkQueue {
    private FirstLastList theList;
    public LinkQueue(){
        theList = new FirstLastList();
    }
    public boolean isEmpty(){
        return theList.isEmpty();
    }
    public void insert(long j){
        theList.insertLast(j);
    }
    public long remove(){
        return theList.deleteFirst();
    }
    public void displayQueue(){
        System.out.print("Queue (front --> rear): ");
        theList.displayList();
    }
}
class Link{
    public long dData;
    public Link next;
    public Link(long d){
        dData = d;
    }
    public void displayLink(){
        System.out.print(dData+" ");
    }
}
class FirstLastList{
    private Link first;
    private Link last;
    public FirstLastList(){
        first = null;
        last = null;
    }
    public boolean isEmpty(){
        return first == null;
    }
    public void insertLast(long dd){
        Link newlink = new Link(dd);
        if(isEmpty()){
            first = newlink;
        }else{
            last.next = newlink;
        }
        last = newlink;
    }
    public long deleteFirst(){
        long temp = first.dData;
        if(first.next==null)
            last = null;
        first = first.next;
        return temp;
    }
    public void displayList(){
        Link current = first;
        while(current!=null){
            current.displayLink();
            current = current.next;
        }
        System.out.println("");
    }
}
public class LinkQueueApp {
    public static void main(String[] args) {
        LinkQueue theQueue = new LinkQueue();
        theQueue.insert(20);
        theQueue.insert(40);
        theQueue.displayQueue();
        theQueue.insert(60);
        theQueue.insert(80);
        theQueue.displayQueue();
        theQueue.remove();
        theQueue.remove();
        theQueue.displayQueue();
    }
}
//輸出:
Queue (front --> rear): 20 40 
Queue (front --> rear): 20 40 60 80 
Queue (front --> rear): 60 80


數據類型和抽象:

        「抽象數據類型」這個術語從何而來?首先看看「數據類型」這部分,再來kao慮「抽象」。

    數據類型

          「數據類型」一詞用在不少地方。它首先表示內置的類型,例如int型和double型。這多是聽到這個詞後首先想到的。

            當談論一個簡單類型時,實際上涉及到兩件事:擁有特定特徵的數據項和在數據上容許的操做。

            隨着面向對象的出現,如今能夠用類來建立本身的數據類型。      

            更普遍的說,當一個數據存儲結構(例如棧和隊列)被表示爲一個類時,它也成了一個數據類型。棧和int類型在不少方面都不一樣,但它們都被定義爲一組具備必定排列規律的數據和在此數據上的操做集合。

   抽象

            抽象是「不kao慮細節的描述和實現」。抽象是事物的本質和重要特徵。

            所以,在面向對象編程中,一個抽象數據類型是一個類,且不kao慮它的實現。它是對類中數據(域)的描述和可以在數據上執行的一系列操做(方法)以及如何使用這些操做的說明。

            當「抽象數據類型」用於棧和隊列這樣的數據結構時,它的意義被進一步擴展了。和其餘類同樣,它意味着數據和在數據上執行的操做,即便子啊這種狀況下,如何存儲數據的基本原則對於類用戶來講也是不可見的。用戶不只不知道方法怎樣運做,也不知道數據是如何存儲的。

    接口

            ADT有一個常常被叫作「接口」的規範。它是給類用戶看的,一般是類的公有方法。在棧中push()方法、pop()方法和其餘相似的方法造成了接口。

ADT列表

            列表(有時也叫線性表)是一組線性排列的數據項。也就是說,它們以必定的方式串接起來,像一根線上的珠子或一條街上的房子。列表支持必定的基本操做。列表支持必定的基本操做。能夠插入某一項,刪除某一項,還有常常從某個特定位置讀出一項(例如,讀出第三項)。

做爲設計工具的ADT

            ADT的概念在軟件設計過程總也是有用的。若是須要存儲數據,那麼就從kao慮須要在數據上實現的操做開始。須要存取最後一個插入的數據項?仍是第一個?是特定值的項?仍是在特定位置的項?回da這些問題會引出ADT的定義。只有在完整定義了ADT後,才應該kao慮細節問題,例如如何表示數據,如何編碼是方法能夠存取數據等等。

            固然,一旦設計好ADT,必須仔細選擇內部的數據結構,以使規定的操做的效率儘量高。例如,若是須要隨機存取元素N,那麼用鏈表表示就不夠好,由於對鏈表來講,隨機訪問不是一個高效的操做。選擇數組會獲得較好的效果。

有序鏈表

            鏈表中,保持數據有序是有用的,具備這個特性的鏈表叫作「有序鏈表」。

            在有序鏈表中,數據是按照關鍵值有序排列的。有序鏈表的刪除經常是隻限於刪除在鏈表頭部最小(或者最大)鏈結點。不過,有時也用find()方法和delete()在整個鏈表中搜索某一特定點。

            通常在大多數須要使用有序數組的場合也可使用有序鏈表。有序鏈表因爲有序數組的地方是插入的速度(由於元素不須要移動),另外鏈表能夠擴展所有有效的使用內存,而數組只能侷限於一個固定的大小。可是,有序鏈表實現起來比有序數組更困難一些。

            後面有一個有序鏈表的應用:爲數據排序。有序鏈表也能夠用於實現優先級隊列,儘管堆是更經常使用的實現方法。

在有序鏈表中插入一個數據項的Java代碼

        爲了在一個有序鏈表中插入數據項,算法必須首先搜索鏈表,直到找合適位置:它剛好在第一個比它大的數據項的前面。

        當算法找到了要插入的位置,用一般的方式插入數據項;把新鏈結點的next字段指向下一個鏈結點,而後把前一個鏈結點的next字段改成指向新的鏈結點。然而,須要kao慮一些特殊狀況:鏈結點可能在表頭,或者插在表尾。

public void insert(long j){
    Link newlink = new Link(j);        //make new link
    Link previous = null;              //start at first
    Link current = first;
                                       //until end of list
    while(current!=null && j >current.dData)
    {                                  //or key > current
        previous = current;
        current = current.next;        //go to next item
    }
    if(previous==null)                 //at beginning of list
        first = newlink;               //first --> newLink
    else
        previous.next = newlink;       //old prev -->newLink
    newlink.next = current;            //newLink --> old current
}        //end insert()

        在鏈表上移動,須要一個previous引用,這樣才能把前一個鏈結點next字段指向新的鏈結點。建立新鏈結點後,把current變量設爲first,準備搜索正確的插入點。這時也把previous設爲null值,這部操做,很重要,由於後面要用這個null值判斷是否仍在表頭。

package com.list.sortedlist;
public class Sortedlist {
    private Link first;
    public Sortedlist(){
        first = null;
    }
    public boolean isEmpty(){
        return first == null;
    }
    public void insert(long key){
        Link newlink = new Link(key);
        Link previous = null;
        Link current = first;
        while(current!=null && key<current.dData){
            previous = current;
            current = current.next;
        }
        if(previous==null){
            first = newlink;
        }else{
            previous.next = newlink;
        }
        newlink.next = current;
    }
    public Link remove(){
        Link temp = first;
        first = first.next;
        return temp;
    }
    public void displayList(){
        System.out.print("List(first -- >): ");
        Link current = first;
        while(current!=null){
            current.displayLink();
            current = current.next;
        }
        System.out.println("");
    }
}
class Link{
    public long dData;
    public Link next;
    public Link(long dd){
        dData = dd;
    }
    public void displayLink(){
        System.out.print(dData+" ");
    }
}
public static void main(String[] args) {
    Sortedlist theSortedlist = new Sortedlist();
    theSortedlist.insert(20);
    theSortedlist.insert(40);
    
    theSortedlist.displayList();
    
    theSortedlist.insert(10);
    theSortedlist.insert(30);
    theSortedlist.insert(50);
    
    theSortedlist.displayList();
    theSortedlist.remove();
    
    theSortedlist.displayList();
}
//輸出:
List(first -- >): 40 20 
List(first -- >): 50 40 30 20 10 
List(first -- >): 40 30 20 10

有序鏈表的效率

在有序鏈表插入和刪除某一項最多須要O(N)次比較(平均N/2),由於必須沿着鏈表上一步一步走才能找到正確的位置。然而,能夠在O(1)的時間內找到或刪除最小值,由於它總在表頭。若是一個應用頻繁地取最小值項,且不須要快速的插入,那麼有序鏈表是一個有效的方法選擇。例如,優先級隊列能夠用有序鏈表來實現。

表插入排序

        有序鏈表能夠用於一種高效的排序機制。假設有一個無序數組。若是從這個數組中取出數據,而後一個一個地插入有序鏈表,它們自動地按順序排列。把它們從有序列表中刪除,從新放入數組,呢麼數組就會排序好了。

        這種排序方式整體上比在數組中用經常使用的插入排序效率更高一些,這是由於這種方式進行的複製次數少一些,它仍然是一個時間級爲O(N²)的過程,由於在有序鏈表中每插入一個新的鏈結點,平均要與一半已存在數據進行比較,若是插入N個新數據,就進行了N²/4次比較。每一鏈結點只要進行兩次複製:一次從數組到列表,一次從鏈表到數組。在數組中進行插入排序須要N²次移動,相比之下,2*N次移動更好。

package redis.list.listinsertionsort;

pubic class Link{
    public long dData;
    public Link next;
    public Link(long dd){
    dData = dd;
    }
}
pubic class SortedList{
    private Link first;
    {
    first = null;
    }
    public SortedList(Link[] linkArr){
        first = null;
        for (int i = 0; i < linkArr.length; i++) {
            insert(linkArr[i]);
        }
    }
    public void insert(Link k){
        Link previous = null;
        Link current = first;
        while(current!=null && k.dData>current.dData){
            previous = current;
            current = current.next;
        }
        if(previous==null)
            first = k;
        else
            previous.next = k;
        k.next = current;
    }
    public Link remove(){
        Link temp = first;
        first = first.next;
        return temp;
    }
}
public static void main(String[] args) {
    int size = 10;
    Link[] linkArray = new Link[size];
    for (int i = 0; i < size; i++) {
        int n = (int)(java.lang.Math.random()*99);
        Link newlink = new Link(n);
        linkArray[i] = newlink;
    }
    System.out.print("Unsorted array:");
    for (int i = 0; i < size; i++) {
        System.out.print(linkArray[i].dData+" ");
    }
    System.out.println("");
    SortedList theSortedList = new SortedList(linkArray);
    for (int i = 0; i < size; i++) {
        linkArray[i] = theSortedList.remove();
    }
    System.out.print("Sorted array:");
    for (int i = 0; i < size; i++) {
        System.out.print(linkArray[i].dData+" ");
    }
    System.out.println("");
}
//輸出:
Unsorted array:53 36 0 91 37 48 2 20 2 34 
Sorted array:  0 2 2 20 34 36 37 48 53 91

            SortedList類的新構造函數把Link對象數組做爲參數讀入,而後把整個數組內容插入到新建立的鏈表中。這樣作之後,有助於簡化客戶(mian()方法)的工做。

        和基於數組的插入排序相比,表插入排序有一個缺點,就是它要開闢差很少兩倍的空間;數組和鏈表必須同時在內存中存在。但若是有現成的有序鏈表類可用,那麼用表插入排序對不太大的數組排序是比較便利的。


雙向鏈表

        雙向鏈表(不是雙端鏈表),雙向鏈表有什麼優勢呢?傳統鏈表的一個潛在問題是沿鏈表的反向遍歷是困難。用這樣一個語句:

        current = current.next;

        能夠很方便地到達下一個鏈結點然而沒有對應的方法回到前一個鏈結點。根據應用的不一樣,這個限制可能會引發問題。

        雙向鏈表提供了回頭方向走一步的操做能力。即容許向前遍歷,也容許向後遍歷整個鏈表。其中祕密在於每一個鏈結點有兩個指向其餘鏈結點的引用,而不是一個。第一個像普通鏈表同樣指向下一個鏈結點。第二個指向前一個鏈結點。

在雙向鏈表中,Link類定義的開頭是這樣聲明的:

class Link{
    public long dData;        //data item
    public Link next;         //next link in list
    public Link previous;     //previous link in list
}

        雙向鏈表的缺點是每次插入或刪除一個鏈結點的時候,要處理四個鏈結點的引用,而不是兩個:

        兩個鏈接前一個的鏈結點,兩個鏈接後一個鏈結點。固然,因爲多了兩個引用,鏈結點的佔用空間也變大了一點。固然,因爲多了兩個引用,鏈結點的佔用空間也變大了一點。

         雙向鏈表沒必要是雙端鏈表(保持一個鏈表最後一個元素的引用),但這種方式是有用的,因此在後面的例子中將包含雙端的性質。

package com.list.doublylinked;
public class DoublyLinked {
}
class Link{
    public long dData;
    public Link next;
    public Link previous;
    public Link(long d){
        dData = d;
    }
    public void displayLink(){
        System.out.print(dData+" ");
    }
}
class DoublyLinkedList{
    private Link first;
    private Link last;
    public DoublyLinkedList(){
        first = null;
        last = null;
    }
    public boolean isEmpty(){
        return first == null;
    }
    public void insertFirst(long dd){
        Link newLink = new Link(dd);
        if(isEmpty()){
            last = newLink;
        }else{
            first.previous = newLink;
        }
        newLink.next = first;
        first = newLink;
    }
    
    public void insertLast(long dd){
        Link newLink = new Link(dd);
        if(isEmpty()){
            first = newLink;
        }else{
            last.next = newLink;
            newLink.previous = last;
        }
        last = newLink;
    }
    
    public Link deleteFirst(){
        Link temp = first;
        if(first.next==null){
            last = null;
        }else{
            first.next.previous = null;
        }
        first = first.next;
        return temp;
    }
    
    public Link deleteLast(){
        Link temp = last;
        if(last.previous==null){
            first = null;
        }else{
            last.previous.next = null;
        }
        last  = last.previous;
        return temp;
    }
    
    public boolean insertAfter(long key,long dd){
        Link current = first;
        while(current.dData!=key){
            current = current.next;
            if(current==null){
                return false;
            }
        }
        Link newlink = new Link(dd);
        if(current==last){
            newlink.next = null;
            last = newlink;
        }else{
            newlink.next = current.next;
            current.next.previous = newlink;
        }
        newlink.previous = current;
        current.next = newlink;
        return true;
    }
    
    public Link deleteKey(long key){
        Link current = first;
        while(current.dData!=key){
            current = current.next;
            if(current==null){
                return null;
            }
        }
        if(current==first){
            first = current.next;
        }else{
            current.previous.next=current.next;
        }
        if(current==last){
            last=current.previous;
        }else{
            current.next.previous = current.previous;
        }
        return current;
    }
    
    public void displayForward(){
        System.out.print("List (first --> last): ");
        Link current = first;
        while(current!=null){
            current.displayLink();
            current = current.next;
        }
        System.out.println("");
    }
    
    public void displayBackward(){
        System.out.print("List (last-->first): ");
        Link current = last;
        while(current!=null){
            current.displayLink();
            current = current.previous;
        }
        System.out.println("");
    }
}
public static void main(String[] args) {
		DoublyLinkedList theList = new DoublyLinkedList();
		theList.insertFirst(22);
		theList.insertFirst(44);
		theList.insertFirst(66);
		
		theList.insertLast(11);
		theList.insertLast(33);
		theList.insertLast(55);
		
		theList.displayForward();
		theList.displayBackward();
		
		theList.deleteFirst();
		theList.deleteLast();
		theList.deleteKey(11);
		
		theList.displayForward();
		
		theList.insertAfter(22, 77);
		theList.insertAfter(33, 88);
		
		theList.displayForward();
		
	}
	
//輸出:
List (first --> last): 66 44 22 11 33 55 
List (last-->first): 55 33 11 22 44 66 
List (first --> last): 44 22 33 
List (first --> last): 44 22 77 33 88

基於雙向鏈表的雙端隊列

        在雙端隊列中,能夠從任何一頭插入和刪除,雙向鏈表提供了這個能力。

迭代器

    放在鏈表內部嗎?

    迭代器類       

    迭代器類包含對數據結構中數據項的引用,並用來遍歷這些結構的對象(有時,在某些Java類中,叫作「枚舉器」)。下面是它們最初的定義:

class ListIterator(){
    private Link current;
    ......
}

       

 。。。。。。

小    結

  • 鏈表包含一個linkedList對象和許多Link對象;

  • linkedList對象包含一個引用。這個引用一般叫作first,它指向鏈表的第一個鏈結點

  • 每一個Link對象包含數據和一個引用,一般叫作next,它指向鏈表的下一個鏈結點。

  • next字段爲null值意味着鏈表的結尾

  • 在表頭插入鏈結點須要把新鏈接點的next字段指向原來的第一個鏈結點,而後發first指向新鏈結點

  • 在表頭刪除鏈結點要把first指向frist next

  • 爲了遍歷鏈表,從first開始,而後從一個鏈結點到下一個鏈結點。一旦找到能夠顯示,刪除或其餘方式操縱給鏈結點

  • 新鏈結點能夠插在某個特定值的鏈結點的前面或後面,首先要遍歷找到這個鏈結點

  • 雙端鏈表在鏈表中維護一個指向最後一個鏈結點的引用,它一般和叫first同樣,叫作last

  • 雙端鏈表容許在表尾插入數據項。

  • 抽象數據類型是一種數據存儲類,不涉及它的實現。

  • 棧和隊列是ADT。它們既能夠用數組實現,又能夠用鏈表實現。

  • 有序鏈表中,鏈結點按照關鍵值升序(有時是降序)排列。

  • 在有序鏈表中須要O(N)的時間,由於必須找到正確的插入點。最小鏈結點的刪除須要O(1)的時間

  • 雙向鏈表中,每一個鏈結點包含對前一個鏈結點的引用,同時有對後一個鏈結點的引用。

  • 雙向鏈表容許反向遍歷,並能夠從表尾刪除

  • 迭代器是一個引用,它被封裝在類對象中,這個引用指向關聯的鏈表中的鏈結點。

  • 迭代器方法容許使用者沿鏈表移動迭代器,並訪問當前指示的鏈結點

  • 能用迭代器遍歷鏈表,在選定的鏈結點(或全部鏈結點)上執行某些操做。