玩轉數據結構——第三章:最基礎的動態數據結構:鏈表

內容概括:

  • 3-1.什麼是鏈表
  • 3-2.在鏈表中添加元素
  • 3-3.使用鏈表的虛擬頭結點
  • 3-4.鏈表的遍歷,查詢和修改
  • 3-5.從鏈表中刪除元素
  • 3-6.使用鏈表實現棧
  • 3-7.帶有尾指針的鏈表:使用鏈表實現隊列

3-1.什麼是鏈表

  • 鏈表:真正的動態數據結構 
  • 最簡單的動態數據結構
  • 更深入的理解引用(或者指針)
  • 更深入的理解遞歸
  • 輔助組成其他數據結構

鏈表Linked List

鏈表像火車一樣,每個節點相當於一節車廂,每一節車廂存放真正的數據,每一節車廂之間要有next鏈接

  • 數據存儲在「節點」(Node)中
  • 如果一個節點的next爲null,則已經到了鏈表末尾了
  • 優點:真正的動態,不需要處理固定容量的問題
  • 缺點:與數組比較喪失了隨機訪問的能力

數組和鏈表相比

組成鏈表的內部節點和next類: 

  • 數組最好用於索引有語意的情況。scores[2]
  • 最大的優點:支持快速查詢
  • 鏈表不適合用於索引有語意的情況
  • 最大優點:動態
public class LinkedList<E> {
    //私有內部類
    private class Node {//只有在鏈表內才能訪問
        public E e;//節點數據
        public Node next;//節點next

        public Node(E e, Node next) {
            this.e = e;//用戶傳進來的e賦值給當前節點的e
            this.next = next;//用戶傳來的next當前節點的next  }

        public Node(E e) {
            this(e, null);  }

        public Node() {
            this(null, null) }
        @Override
        public String toString(){
            return e.toString();}

    }
    private Node head;//鏈表頭
    private int size;//鏈表長度

    public LinkedList(){
        head=null;
        size=0; }
    //獲取鏈表中元素的個數
    public int getSize(){
        return size; }
        //返回鏈表是否爲空
    public boolean isEmpty(){
        return size==0; }

}

3-2.在鏈表中添加元素

1.在鏈表頭添加元素

 

//在鏈表頭添加新的元素
    public void addFirst(E e){
//        Node node=new Node();
////        node.next=head;
////        head=node;
        head=new Node(e,head);//當前節點爲用戶傳進來的節點,當前節點的next爲用戶上一個頭結點
        size++;
    }

 2.在鏈表中間添加元素

 如果順序調換。

  • 關鍵:找到要添加的節點的前一個節點
//在鏈表的index(0-based)位置添加新的元素e
    //在鏈表中不是一個常用操作,練習用:
    public void add(int index,E e){
        if(index<0||index>size){//判斷index的合法性
            throw  new IllegalArgumentException("Add is Fail,Illega Index"); }
        if(index==0)
            addFirst(e);
        else{
            Node prev=head;
             for (int i=0;i<index-1;i++){//讓prev找到index-1位置
                 prev=prev.next;//讓prev挪向待插入位置的前一個節點

//                 Node node =new Node(e);//1
//                 node.next=prev.next;//2
//                 prev.next=node;//3
                 //new Node(e,prev.next);完成前兩句話的任務
                 //prev.next=..完成第三句話的任務
                 prev.next=new Node(e,prev.next);
                 size++;
             }}}
 //在鏈表的末尾添加新的元素e
    public void addLast(E e){
        add(size,e);
    }

3-3.鏈表設置虛擬頭結點

元素從0開始,dummyHead是0位置的前一個節點

  • 在沒有虛擬頭結點的鏈表中鏈表頭添加元素和鏈表任意位置上添加元素的操作不同
  • 因爲鏈表頭沒有前一個節點,爲了實現兩個操作統一,設置一個虛擬頭結點。
public LinkedList(){
        dummyHead=new Node(null,null);//創建一個虛擬頭結點
        size=0; }
    //獲取鏈表中元素的個數
    public int getSize(){
        return size; }
        //返回鏈表是否爲空
    public boolean isEmpty(){
        return size==0; }


    //在鏈表的index(0-based)位置添加新的元素e
    //在鏈表中不是一個常用操作,練習用:
    public void add(int index,E e){
        if(index<0||index>size){//判斷index的合法性
            throw  new IllegalArgumentException("Add is Fail,Illega Index");
        }
            Node prev=dummyHead;//虛擬頭結點,0位置的元素的前一個節點
             for (int i=0;i<index;i++)//讓prev找到index位置的前一個位置
                 prev=prev.next;//讓prev挪向待插入位置的前一個節點
                 prev.next=new Node(e,prev.next);
                 size++;
             
    }
    //在鏈表頭添加新的元素
    public void addFirst(E e){
        add(0,e);
    }
    //在鏈表的末尾添加新的元素e
    public void addLast(E e){
        add(size,e);
    }

3-4.鏈表的遍歷、查詢和修改

 獲得鏈表的第index個元素

//獲得鏈表的第index(0-based)個位置的元素
    public E get(int index){
        //判斷index的合法性
        if(index<0||index>size){//判斷index的合法性
            throw  new IllegalArgumentException("Get is Fail,Illega Index");
        }
        Node cur=dummyHead.next;//從dummyHead的下一個節點開始遍歷
        for(int i=0;i<index;i++)
            cur=cur.next;
        return cur.e;//返回的就是index位置的元素
    }
    //獲得鏈表的第一個元素
    public E getFirst(){
        return get(0);
    }
    //獲得鏈表的最後一個元素
    public E getLast(){
        return  get(size-1);//從0開始算的 0個位置有一個元素 size=1
    }

修改index位置的元素

//修改鏈表的第index(0-based)個位置的元素
    public void set(int index,E e){
        //判斷index的合法性
        if(index<0||index>size){//判斷index的合法性
            throw  new IllegalArgumentException("Set is Fail,Illega Index");
        }
        Node cur=dummyHead.next;
        for (int i=0;i<index;i++)
            cur=cur.next;
        cur.e=e;//讓第index位置的e變成新的額
    }

查找鏈表有無此元素

//查找鏈表是否有元素e
    public boolean contains(E e) {
        Node cur = dummyHead.next;
        while (cur != null) {//沒到末尾
            if (cur.e.equals(e))//
                return true;
            cur = cur.next;//否則繼續往下找

        }
        return false;//找不到
    }
    @Override
    public String toString(){
        StringBuilder res=new StringBuilder();
        Node cur=dummyHead.next;
       // for(Node cur=dummyHead.next;cur!=null;cur=cur.next)等價
        while(cur!=null){
            res.append(cur+"->");
            cur=cur.next;
        }
        res.append("NULL");//到達節尾
        return res.toString();
    }

Main_Activity.java中實現

public class Main {

    public static void main(String[] args) {
     LinkedList<Integer> linkedList=new LinkedList<>();
	for(int i=0;i<5;i++){
	    linkedList.addFirst(i);
	    System.out.println(linkedList);
    }
    linkedList.add(2,666);//在索引爲2的位置添加元素666
        System.out.println(linkedList);
    }
}

結果

0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->666->2->1->0->NULL

3-5.從鏈表中刪除元素

鏈表刪除操作的實現: 

//從鏈表中刪除index元素的草(0-based)
    public E remove(int index){
        //判斷index的合法性
        if(index<0||index>size){//判斷index的合法性
            throw  new IllegalArgumentException("Set is Fail,Illega Index");
        }
        Node prev=dummyHead;
        for (int i=0;i<index;i++){
            prev=prev.next;//存待刪除之前的節點
        }
        Node retNode=prev.next;//要刪除的節點retNode
        prev.next=retNode.next;//跳過retNode
        retNode=null;
        size--;
        return retNode.e;//返回刪除的的元素
    }
    //刪除第一個元素
    public E removeFirst(){
        return remove(0);
    }
    //刪除最後一個元素
    public E removeLast(){
        return remove(size-1);
    }

Main實現

public class Main {

    public static void main(String[] args) {
     LinkedList<Integer> linkedList=new LinkedList<>();
	for(int i=0;i<5;i++){
	    linkedList.addFirst(i);
	    System.out.println(linkedList); }
    linkedList.add(2,666);//在索引爲2的位置添加元素666
        System.out.println(linkedList);
        //刪除2的元素
        linkedList.remove(2);
        System.out.println(linkedList);
        //刪除第一個元素
        linkedList.removeFirst();
        System.out.println(linkedList);
        //刪除最後的元素
        linkedList.removeLast();
        System.out.println(linkedList);
    }
}

 結果

0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->666->2->1->0->NULL
4->3->2->1->0->NULL
3->2->1->0->NULL
3->2->1->NULL

鏈表的時間複雜度分析:

添加操作:O(n)

  • addLast(e)   O(n)
  • addFirst(e)   O(1)
  • add(index,e) O(n/2)=O(n)

刪除操作:O(n)

  • removeLast(e)   O(n)
  • removeFirst(e)   O(1)
  • remove(index,e) O(n/2)=O(n)

修改操作:O(n)

  • set(index,e)  O(n)

查找操作: O(n)

  • get(e)           O(n)
  • contains(e)   O(n)

3-6.使用鏈表實現棧

要想實現上一節只對鏈表頭進行操作,可以將鏈表實現棧,將鏈表頭看做棧頂

public interface Stack<E> {
    int getSize();//返回棧的元素個數
    boolean isEmpty();//返回棧是否爲空
    void push(E e);//入棧
    E pop();//出棧
    E peek();//查看棧末尾的元素
}
public class LinkedListStack<E> implements Stack<E>{
    private LinkedList<E> list;

    public  LinkedListStack(){
        list=new LinkedList<>();

    }
    @Override
    public int getSize(){
        return list.getSize();
    }

    @Override
    public boolean isEmpty(){
        return list.isEmpty();
    }

    //給棧頂添加一個元素
    @Override
    public  void push(E e){
        list.addFirst(e);//鏈表頭是棧頂
    }

    //從棧頂取出元素
    @Override
    public E pop(){
        return list.removeFirst();
    }
    //看棧頂的元素
    @Override
    public E peek(){
        return list.getFirst();
    }
    @Override
    public String toString(){
        StringBuilder res=new StringBuilder();
        res.append("Stack:top ");
        res.append(list);
        return res.toString();
    }
    //測試用例
    public static void main(String[] args) {
        LinkedListStack<Integer> stack=new LinkedListStack<>();
        for(int i=0;i<5;i++){
            stack.push(i);//入棧
            System.out.println(stack);
        }
        stack.pop();//出棧
        System.out.println(stack);
    }
}

結果:

Stack:top 0->NULL
Stack:top 1->0->NULL
Stack:top 2->1->0->NULL
Stack:top 3->2->1->0->NULL
Stack:top 4->3->2->1->0->NULL
Stack:top 3->2->1->0->NULL

數組棧和鏈表的棧的比較

public class MainTest {//Queue的多態性
    //測試使用q運行opCount個enqueue和dequeue操作兩種隊列需要花費的時間,單位秒
    private static double testStack(Stack<Integer> stack,int opCount){
        long startTime=System.nanoTime();//計算當時時間,單位納秒
        //。。。。。你要進行的操作
        //生成隨機數
        Random random=new Random();
       for(int i=0;i<opCount;i++){
           //入隊插入隨機數
           stack.push(random.nextInt(Integer.MAX_VALUE));//0-Integer最大值
       }
        for(int i=0;i<opCount;i++){
            //出隊操作
            stack.pop();
        }

        long endTime=System.nanoTime();//計算結束時間,單位納秒

       return (endTime-startTime)/1000000000.0;//轉換單位成秒
    }
    public static void main(String[] args) {

        int opCount=100000;

        //數組棧的操作
        ArrayStack<Integer> arrayStack=new ArrayStack<>();
        double time1=testStack(arrayStack,opCount);
        System.out.println("ArrayStack Time="+time1+"s");

        //鏈表實現的棧的操作
        LinkedListStack<Integer> linkedListStack=new LinkedListStack<>();
        double time2=testStack(linkedListStack,opCount);
        System.out.println("LinkedListStack Time="+time2+"s");


    }
}

結果:並不是所有情況LinkedListStack都比ArrayStack時間快

ArrayStack Time=0.037542493s
LinkedListStack Time=0.021257096s

3-7.帶有尾指針的鏈表:使用鏈表實現隊列

在隊尾中添加一個tail屬性,來控制元素的入隊操作,時間複雜度爲O(1)

head屬性來控制出隊操作,時間複雜度也爲O(1)

基本屬性

public class LinkedListQueue<E> implements Queue<E> {
    //私有內部類
    private class Node {//只有在鏈表內才能訪問
        public E e;//節點數據
        public Node next;//節點next

        public Node(E e, Node next) {
            this.e = e;//用戶傳進來的e賦值給當前節點的e
            this.next = next;//用戶傳來的next當前節點的next }

        public Node(E e) {
            this(e, null);      }
        public Node() {
            this(null, null);   }
        @Override
        public String toString() {
            return e.toString(); }}

    private Node head, tail;//頭尾節點
    private int size;
    public LinkedListQueue() {
        head = null;
        tail = null;
        size = 0;   }
    @Override
    public int getSize() {
        return size;  }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }}

進隊操作

//進隊操作、在隊尾中插入元素
    @Override
    public void enqueue(E e){
        if (tail == null) {//鏈表尾如爲空,則隊首head也爲空
            tail = new Node(e);
            head = tail;
        } else {
            tail.next = new Node(e);
            tail = tail.next;
        }
        size++;
    }

出隊操作

//出隊操作,從隊首
    @Override
    public E dequeue(){
        if(isEmpty())
            throw  new IllegalArgumentException("Cannot dequeue from an Empty queue");
       Node reNode=head;
       head=head.next;
       reNode.next=null;
       if(head==null)
           tail=null;
       size--;
       return reNode.e;
    }
    //獲取隊首元素
    @Override
    public E getFront(){
        if(isEmpty())
            throw  new IllegalArgumentException("Queue is empty");
        return  head.e;
    }

重寫toString方法 

@Override
    public String toString(){
        StringBuilder res=new StringBuilder();
        res.append("Queue:front ");
        Node cur=head;
        while (cur!=null){
            res.append(cur+"->");
            cur=cur.next;
        }
        res.append("NULL tail");
        return res.toString();
    }

測試方法: 

public static  void main(String[] arg){
        LinkedListQueue<Integer> queue=new LinkedListQueue<>();
        for(int i=0;i<10;i++){
            queue.enqueue(i);
            System.out.println(queue);
            //每插入隊列3個元素,取出一個元素
            if(i%3==2){//0、1、2 2對3取餘爲2
                queue.dequeue();
                System.out.println(queue);
            }  } }

結果:

Queue:front 0->NULL tail
Queue:front 0->1->NULL tail
Queue:front 0->1->2->NULL tail
Queue:front 1->2->NULL tail
Queue:front 1->2->3->NULL tail
Queue:front 1->2->3->4->NULL tail
Queue:front 1->2->3->4->5->NULL tail
Queue:front 2->3->4->5->NULL tail
Queue:front 2->3->4->5->6->NULL tail
Queue:front 2->3->4->5->6->7->NULL tail
Queue:front 2->3->4->5->6->7->8->NULL tail
Queue:front 3->4->5->6->7->8->NULL tail
Queue:front 3->4->5->6->7->8->9->NULL tail