Java數據結構與算法分析 | 鏈表(單鏈表、雙鏈表、環形鏈表)

GitHub源碼分享

項目主頁: https://github.com/gozhuyinglong/blog-demos
本文源碼: https://github.com/gozhuyinglong/blog-demos/tree/main/java-data-structures/src/main/java/com/github/gozhuyinglong/datastructures/linkedlist

1. 前言

經過前篇文章《數組》瞭解到數組的存儲結構是一塊連續的內存,插入和刪除元素時其每一個部分都有可能總體移動。爲了不這樣的線性開銷,咱們須要保證數據能夠不連續存儲。本篇介紹另外一種數據結構:鏈表。java

2. 鏈表(Linked List)

鏈表是一種線性的數據結構,其物理存儲結構是零散的,數據元素經過指針實現鏈表的邏輯順序。鏈表由一系列結點(鏈表中每個元素稱爲節點)組成,節點能夠在內存中動態生成。node

鏈表的特性:git

  • 鏈表是以節點(Node)的方式來存儲,因此又叫鏈式存儲。
  • 節點能夠連續存儲,也能夠不連續存儲。
  • 節點的邏輯順序與物理順序能夠不一致
  • 表能夠擴充(不像數組那樣還得從新分配內存空間)

鏈表分爲單鏈表、雙鏈表和環形鏈表,下面經過實例逐個介紹。github

3. 單鏈表(Singly Linked List)

單鏈表又叫單向鏈表,其節點由兩部分構成:算法

  • data域:數據域,用來存儲元素數據
  • next域:用於指向下一節點

單鏈表的結構以下圖:編程

單鏈表.jpg

3.1 單鏈表的操做

單鏈表的全部操做都是從head開始,head自己不存儲元素,其next指向第一個節點,而後順着next鏈進行一步步操做。其尾部節點的next指向爲空,這也是判斷尾部節點的依據。數組

這裏主要介紹插入和刪除節點的操做。數據結構

3.1.1 插入節點

向單鏈表中插入一個新節點,能夠經過調整兩次next指向來完成。以下圖所示,X爲新節點,將其next指向爲A2,再將A1的next指向爲X便可。ide

如果從尾部節點插入,直接將尾部節點的next指向新節點便可。this

單鏈表-插入節點.jpg

3.1.2 刪除節點

從單鏈表中刪除一個節點,能夠經過修改next指向來實現,以下圖所示,將A1的next指向爲A3,這樣便刪除A2,A2的內存空間會自動被垃圾回收。

如果刪除尾部節點,直接將上一節點的next指向爲空便可。

單鏈表-刪除節點.jpg

3.2 代碼實現

咱們使用Java代碼來實現一個單鏈表。其中Node類存儲單鏈表的一個節點,SinglyLinkedList類實現了單鏈表的全部操做方法。
SinglyLinkedList類使用帶頭節點的方式實現,即head節點,該節點不存儲數據,只是標記單鏈表的開始。

public class SinglyLinkedListDemo {

    public static void main(String[] args) {
        Node node1 = new Node(1, "張三");
        Node node2 = new Node(3, "李四");
        Node node3 = new Node(7, "王五");
        Node node4 = new Node(5, "趙六");

        SinglyLinkedList singlyLinkedList = new SinglyLinkedList();
        System.out.println("-----------添加節點(尾部)");
        singlyLinkedList.add(node1);
        singlyLinkedList.add(node2);
        singlyLinkedList.add(node3);
        singlyLinkedList.add(node4);
        singlyLinkedList.print();

        System.out.println("-----------獲取某個節點");
        Node node = singlyLinkedList.get(3);
        System.out.println(node);

        singlyLinkedList.remove(node3);
        System.out.println("-----------移除節點");
        singlyLinkedList.print();

        System.out.println("-----------修改節點");
        singlyLinkedList.update(new Node(5, "趙六2"));
        singlyLinkedList.print();

        System.out.println("-----------按順序添加節點");
        Node node5 = new Node(4, "王朝");
        singlyLinkedList.addOfOrder(node5);
        singlyLinkedList.print();

    }


    private static class SinglyLinkedList {

        // head節點是單鏈表的開始,不用來存儲數據
        private Node head = new Node(0, null);

        /**
         * 將節點添加到尾部
         *
         * @param node
         */
        public void add(Node node) {
            Node temp = head;
            while (true) {
                if (temp.next == null) {
                    temp.next = node;
                    break;
                }
                temp = temp.next;
            }
        }

        /**
         * 按順序添加節點
         *
         * @param node
         */
        public void addOfOrder(Node node) {
            Node temp = head;
            while (true) {
                if (temp.next == null) {
                    temp.next = node;
                    break;
                } else if(temp.next.key > node.getKey()){
                    node.next = temp.next;
                    temp.next = node;
                    break;
                }
                temp = temp.next;
            }
        }

        /**
         * 獲取某個節點
         *
         * @param key
         * @return
         */
        public Node get(int key) {
            if (head.next == null) {
                return null;
            }
            Node temp = head.next;
            while (temp != null) {
                if (temp.key == key) {
                    return temp;
                }
                temp = temp.next;
            }
            return null;
        }

        /**
         * 移除一個節點
         *
         * @param node
         */
        public void remove(Node node) {
            Node temp = head;
            while (true) {
                if (temp.next == null) {
                    break;
                }
                if (temp.next.key == node.key) {
                    temp.next = temp.next.next;
                    break;
                }
                temp = temp.next;
            }
        }

        /**
         * 修改一個節點
         *
         * @param node
         */
        public void update(Node node) {
            Node temp = head.next;
            while (true) {
                if (temp == null) {
                    break;
                }
                if (temp.key == node.key) {
                    temp.value = node.value;
                    break;
                }
                temp = temp.next;
            }
        }

        /**
         * 打印鏈表
         */
        public void print() {
            Node temp = head.next;
            while (temp != null) {
                System.out.println(temp.toString());
                temp = temp.next;
            }
        }

    }


    private static class Node {
        private final int key;
        private String value;
        private Node next;

        public Node(int key, String value) {
            this.key = key;
            this.value = value;
        }

        public int getKey() {
            return key;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        public Node getNext() {
            return next;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    ", value='" + value + '\'' +
                    '}';
        }
    }
}

輸出結果:

-----------添加節點(尾部)
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------獲取某個節點
Node{key=3, value='李四'}
-----------移除節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=5, value='趙六'}
-----------修改節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=5, value='趙六2'}
-----------按順序添加節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=4, value='王朝'}
Node{key=5, value='趙六2'}

3.3 單鏈表的缺點

經過對單鏈表的分析,能夠看出單鏈表有以下缺點:
(1)單鏈表的查找方法只能是一個方向
(2)單鏈表不能自我刪除,須要靠上一節點進行輔助操做。

而這些缺點能夠經過雙鏈表來解決,下面來看詳細介紹。

4. 雙鏈表(Doubly Linked List)

雙鏈表又叫雙向鏈表,其節點由三部分構成:

  • prev域:用於指向上一節點
  • data域:數據域,用來存儲元素數據
  • next域:用於指向下一節點

雙鏈表的結構以下圖:

雙鏈表.jpg

4.1 雙鏈表的操做

雙鏈表的操做能夠從兩端開始,從第一個節點經過next指向能夠一步步操做到尾部,從最後一個節點經過prev指向能夠一步步操做到頭部。

這裏主要介紹插入和刪除節點的操做。

4.1.1 插入節點

向雙鏈表中插入一個新節點,須要經過調整兩次prev指向和兩次next指向來完成。以下圖所示,X爲新節點,將A1的next指向X,將X的next指向A2,將A2的prev指向X,將X的prev指向A1便可。

雙鏈表-插入節點.jpg

4.1.2 刪除節點

從雙鏈表中刪除一個節點,須要經過調整一次prev指向和一次next指向來完成。以下圖所示,刪除A2節點,將A1的next指向A3,將A3的 prev指向A1便可。

雙鏈表-刪除節點.jpg

4.2 代碼實現

咱們使用Java代碼來實現一個雙鏈表。其中 Node類存儲雙鏈表的一個節點,DoublyLinkedListDemo類實現雙鏈表的全部操做方法。
DoublyLinkedListDemo類使用不帶頭節點的方式實現,其中first爲第一個節點,last爲最後一個節點。這兩個節點默認都爲空,若只有一個元素時,則兩個節點指向同一元素。

public class DoublyLinkedListDemo {

    public static void main(String[] args) {

        DoublyLinkedList doublyLinkedList = new DoublyLinkedList();

        System.out.println("-----------從尾部添加節點");
        doublyLinkedList
                .addToTail(new Node(1, "張三"))
                .addToTail(new Node(3, "李四"))
                .addToTail(new Node(7, "王五"))
                .addToTail(new Node(5, "趙六"))
                .print();

        System.out.println("-----------從頭部添加節點");
        doublyLinkedList
                .addToHead(new Node(0, "朱開山"))
                .print();

        System.out.println("-----------獲取某個節點");
        System.out.println(doublyLinkedList.get(3));

        System.out.println("-----------移除節點");
        doublyLinkedList
                .remove(new Node(3, "李四"))
                .print();

        System.out.println("-----------修改節點");
        doublyLinkedList
                .update(new Node(5, "趙六2")).print();

        System.out.println("-----------按順序添加節點");
        doublyLinkedList
                .addOfOrder(new Node(4, "王朝"))
                .print();
    }

    private static class DoublyLinkedList {

        private Node first = null; // first節點是雙鏈表的頭部,即第一個節點
        private Node last = null; // tail節點是雙鏈表的尾部,即最後一個節點

        /**
         * 從尾部添加
         *
         * @param node
         */
        public DoublyLinkedList addToTail(Node node) {
            if (last == null) {
                first = node;
            } else {
                last.next = node;
                node.prev = last;
            }
            last = node;
            return this;
        }

        /**
         * 按照順序添加
         *
         * @param node
         */
        public DoublyLinkedList addOfOrder(Node node) {
            if (first == null) {
                first = node;
                last = node;
                return this;
            }

            // node比頭節點小,將node設爲頭節點
            if (first.key > node.key) {
                first.prev = node;
                node.next = first;
                first = node;
                return this;
            }

            // node比尾節點大,將node設爲尾節點
            if (last.key < node.key) {
                last.next = node;
                node.prev = last;
                last = node;
                return this;
            }

            Node temp = first.next;
            while (true) {
                if (temp.key > node.key) {
                    node.next = temp;
                    node.prev = temp.prev;
                    temp.prev.next = node;
                    temp.prev = node;
                    break;
                }
                temp = temp.next;
            }
            return this;
        }

        /**
         * 從頭部添加
         *
         * @param node
         */
        public DoublyLinkedList addToHead(Node node) {
            if (first == null) {
                last = node;
            } else {
                node.next = first;
                first.prev = node;
            }
            first = node;
            return this;
        }

        /**
         * 獲取節點
         *
         * @param key
         * @return
         */
        public Node get(int key) {
            if (first == null) {
                return null;
            }
            Node temp = first;
            while (temp != null) {
                if (temp.key == key) {
                    return temp;
                }
                temp = temp.next;
            }
            return null;
        }

        /**
         * 移除節點
         *
         * @param node
         */
        public DoublyLinkedList remove(Node node) {
            if (first == null) {
                return this;
            }
            // 要移除的是頭節點
            if (first == node) {
                first.next.prev = null;
                first = first.next;
                return this;
            }
            // 要移除的是尾節點
            if (last == node) {
                last.prev.next = null;
                last = last.prev;
                return this;
            }

            Node temp = first.next;
            while (temp != null) {
                if (temp.key == node.key) {
                    temp.prev.next = temp.next;
                    temp.next.prev = temp.prev;
                    break;
                }
                temp = temp.next;
            }
            return this;
        }

        /**
         * 修改某個節點
         *
         * @param node
         */
        public DoublyLinkedList update(Node node) {
            if (first == null) {
                return this;
            }

            Node temp = first;
            while (temp != null) {
                if (temp.key == node.key) {
                    temp.value = node.value;
                    break;
                }
                temp = temp.next;
            }
            return this;
        }

        /**
         * 打印鏈表
         */
        public void print() {
            if (first == null) {
                return;
            }
            Node temp = first;
            while (temp != null) {
                System.out.println(temp);
                temp = temp.next;
            }
        }
    }

    private static class Node {
        private final int key;
        private String value;
        private Node prev; // 指向上一節點
        private Node next; // 指向下一節點

        public Node(int key, String value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    ", value='" + value + '\'' +
                    '}';
        }
    }
}

輸出結果:

-----------從尾部添加節點
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------從頭部添加節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=3, value='李四'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------獲取某個節點
Node{key=3, value='李四'}
-----------移除節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=7, value='王五'}
Node{key=5, value='趙六'}
-----------修改節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=7, value='王五'}
Node{key=5, value='趙六2'}
-----------按順序添加節點
Node{key=0, value='朱開山'}
Node{key=1, value='張三'}
Node{key=4, value='王朝'}
Node{key=7, value='王五'}
Node{key=5, value='趙六2'}

5. 環形鏈表(Circular Linked List)

環形鏈表又叫循環鏈表,本文講述的是環形單向鏈表,其與單鏈表的惟一區別是尾部節點的next再也不爲空,則是指向了頭部節點,這樣便造成了一個環。

環形鏈表的結構以下圖:

環形鏈表.jpg

5.1 約瑟夫問題

約瑟夫問題:有時也稱爲約瑟夫斯置換,是一個計算機科學和數學中的問題。在計算機編程的算法中,相似問題又稱爲約瑟夫環。又稱「丟手絹問題」。

引自百度百科

聽說著名猶太曆史學家Josephus有過如下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,因而決定了一個自殺方式,41我的排成一個圓圈,由第1我的開始報數,每報數到第3人該人就必須自殺,而後再由下一個從新報數,直到全部人都自殺身亡爲止。然而Josephus 和他的朋友並不想聽從。首先從一我的開始,越過k-2我的(由於第一我的已經被越過),並殺掉第 k我的。接着,再越過k-1我的,並殺掉第 k我的。這個過程沿着圓圈一直進行,直到最終只剩下一我的留下,這我的就能夠繼續活着。問題是,給定了和,一開始要站在什麼地方纔能避免被處決。Josephus要他的朋友先僞裝聽從,他將朋友與本身安排在第16個與第31個位置,因而逃過了這場死亡遊戲。

17世紀的法國數學家加斯帕在《數目的遊戲問題》中講了這樣一個故事:15個教徒和15 個非教徒在深海上遇險,必須將一半的人投入海中,其他的人才能倖免於難,因而想了一個辦法:30我的圍成一圓圈,從第一我的開始依次報數,每數到第九我的就將他扔入大海,如此循環進行直到僅餘15我的爲止。問怎樣排法,才能使每次投入大海的都是非教徒。

問題分析與算法設計

約瑟夫問題並不難,但求解的方法不少;題目的變化形式也不少。這裏給出一種實現方法。

題目中30我的圍成一圈,於是啓發咱們用一個循環的鏈來表示,可使用結構數組來構成一個循環鏈。結構中有兩個成員,其一爲指向下一我的的指針,以構成環形的鏈;其二爲該人是否被扔下海的標記,爲1表示還在船上。從第一我的開始對還未扔下海的人進行計數,每數到9時,將結構中的標記改成0,表示該人已被扔下海了。這樣循環計數直到有15我的被扔下海爲止。

自殺順序.jpg

5.2 代碼實現

咱們使用Java代碼來實現一個環形鏈表,並將節點按約瑟夫問題順序出列。

public class CircularLinkedListDemo {

    public static void main(String[] args) {

        CircularLinkedList circularLinkedList = new CircularLinkedList();

        System.out.println("-----------添加10個節點");
        for (int i = 1; i <= 10; i++) {
            circularLinkedList.add(new Node(i));
        }
        circularLinkedList.print();

        System.out.println("-----------按約瑟夫問題順序出列");
        circularLinkedList.josephusProblem(3);

    }

    private static class CircularLinkedList {
        private Node first = null; // 頭部節點,即第一個節點

        /**
         * 添加節點,並將新添加的節點的next指向頭部,造成一個環形
         *
         * @param node
         * @return
         */
        public void add(Node node) {
            if (first == null) {
                first = node;
                first.next = first;
                return;
            }

            Node temp = first;
            while (true) {
                if (temp.next == null || temp.next == first) {
                    temp.next = node;
                    node.next = first;
                    break;
                }
                temp = temp.next;
            }
        }

        /**
         * 按約瑟夫問題順序出列
         * 即從第1個元素開始報數,報到num時當前元素出列,而後從新從下一個元素開始報數,直至全部元素出列
         *
         * @param num 表示報幾回數
         */
        public void josephusProblem(int num) {
            Node currentNode = first;
            // 將當前節點指向最後一個節點
            do {
                currentNode = currentNode.next;
            } while (currentNode.next != first);

            // 開始出列
            while (true) {
                // 當前節點要指向待出列節點的前一節點(雙向環形隊列不須要)
                for (int i = 0; i < num - 1; i++) {
                    currentNode = currentNode.next;
                }
                System.out.printf("%s\t", currentNode.next.no);
                if(currentNode.next == currentNode){
                    break;
                }
                currentNode.next = currentNode.next.next;
            }
        }

        /**
         * 輸出節點
         */
        public void print() {
            if (first == null) {
                return;
            }

            Node temp = first;
            while (true) {
                System.out.printf("%s\t", temp.no);
                if (temp.next == first) {
                    break;
                }
                temp = temp.next;
            }
            System.out.println();
        }
    }

    private static class Node {
        private final int no;
        private Node next; // 指向下一節點

        public Node(int no) {
            this.no = no;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "no=" + no +
                    '}';
        }
    }
}

輸出結果:

-----------添加10個節點
1    2    3    4    5    6    7    8    9    10    
-----------按約瑟夫問題順序出列
3    6    9    2    7    1    8    5    10    4
相關文章
相關標籤/搜索