1. 程式人生 > >資料結構---連結串列及約瑟夫環問題帶來的思考

資料結構---連結串列及約瑟夫環問題帶來的思考

連結串列和陣列一樣也是線性表的一種。和陣列不同,它不需要再記憶體中開闢連續的空間。

連結串列通過指標將一組零散的記憶體塊連線在一起。我們把記憶體塊稱為連結串列的“結點”(是節點還是結點,結點連線起來打個結所以叫“結點”?開個玩笑),也就是說這些結點可以在記憶體的任意地方,只要有其他的結點的指標指向這個位置就可以。

連結串列又分為單向連結串列,雙向連結串列,迴圈連結串列

 

單向連結串列

 

 

迴圈連結串列:最後一個節點指向第一個結點

 

 

 

雙向連結串列:比單向連結串列多了一個前驅指標,指向前面一個結點

 

 

 

 

從上面的結構和記憶體中的儲存結構來看,就可以發現連結串列相比陣列來說,隨機查詢效率是O(n),只有從頭結點開始查詢;但是它的插入修改效率比陣列高,找到位置之後只需要修改下指標指向,而不需要進行後續元素的遷移。理論上來說是無限容量,不像陣列滿了還需要擴容,擴容還要重新申請記憶體,然後遷移資料,連結串列只需要在記憶體找個一小塊空地,放好資料,讓前面那個指向這裡就是了。

我們也可以看到雙向連結串列比單向連結串列更靈活,因為通過一個結點可以找到前後兩個結點。但是多了個指標所佔空間肯定比單向連結串列大。

 

 

Java中LinkedList就是一個雙向連結串列。

 

 

 

 

簡單實現一個單向連結串列

 

package com.nijunyang.algorithm.link;

/**
 * Description:
 * Created by nijunyang on 2020/3/31 22:09
 */
public class MyLinkedList<E> {
    private Node<E> head;
    private int size = 0;

    /**
     * 頭部插入O(1)
     * @param data
     */
    public void insertHead(E data){
        Node newNode = new Node(data);
        newNode.next = head;
        head = newNode;
        size++;
    }

    public void insert(E data,int position){
        if(position == 0) {
            insertHead(data);
        }else{
            Node cur = head;
            for(int i = 1; i < position ; i++){
                cur = cur.next;        //一直往後遍歷
            }
            Node newNode = new Node(data);
            //
            newNode.next = cur.next;        //新加的點指向後面 保證不斷鏈
            cur.next = newNode;            //把當前的點指向新加的點
            size++;
        }

    }


    public void deleteHead(){
        head = head.next;
        size--;
    }

    public void delete(int position){
        if(position == 0) {
            deleteHead();
        }else{
            Node cur = head;
            for(int i = 1; i < position ; i ++){
                cur = cur.next;  //找到刪除位置的前一個結點
            }
            cur.next = cur.next.next; //cur.next 表示的是刪除的點,後一個next就是我們要指向的
            size--;
        }

    }

    public int size( ){
        return size;
    }

    public String toString( ){
        if (size == 0) {
            return "[]";
        }
        StringBuilder sb = new StringBuilder();
        sb.append('[');
        Node<E> node = head;
        sb.append(node.value);
        int counter = 0;
        for (;;) {
            if (++counter == size) {
                break;
            }
            sb.append(",");
            node = node.next;
            sb.append(node.value);


        }
        sb.append(']');
        return sb.toString();
    }

    public static void main(String[] args) {
        MyLinkedList myList = new MyLinkedList();
        myList.insertHead(5);
        System.out.println(myList);
        myList.insertHead(7);
        System.out.println(myList);
        myList.insertHead(10);
        System.out.println(myList);
        myList.delete(0);
        System.out.println(myList);
        myList.deleteHead();
        System.out.println(myList);
        myList.insert(11, 1);
        System.out.println(myList);

    }

    private static class Node<E>{

        E value;        //值
        Node<E> next;        //下一個的指標

        public Node() {
        }

        public Node(E value) {
            this.value = value;
        }
    }
}

 

 

 

 約瑟夫環問題:

說到連結串列就要提一個下約瑟夫環問題:N個人圍成一圈,第一個人從1開始報數,報M的被殺掉,下一個人接著從1開始報,迴圈反覆,直到剩下最後一個。看到這個就想到用迴圈連結串列來實現,無限remove知道連結串列只剩下一個為止。

之前去力扣上面做的時候就用迴圈連結串列實現了下,驗證是可以過的,但是程式碼提交之後顯示超時,過不了。仔細分析之後之後發現迴圈連結串列實現,時間線複雜度是O(n*m),如果資料大了,必定是個問題。然後就換了,陣列(ArrayList)來實現,每次移除之後大小減一,通過取模size來實現迴圈報數的效果。會發現陣列實現的,時間複雜度僅僅是O(n),兩種方式程式碼如下:

 

package com.nijunyang.algorithm.link;

import java.util.ArrayList;

/**
 * Description:
 * Created by nijunyang on 2020/3/30 21:49
 */
public class Test {

    public static void main(String[] args){
        long start = System.currentTimeMillis();
        int result = yuesefuhuan_link(70866, 116922);
        long end = System.currentTimeMillis();
        System.out.println(result);
        System.out.println("連結串列耗時:" + (end - start));
        System.out.println("-------------------------");

        start = System.currentTimeMillis();
        result = yuesefuhuan_arr(70866, 116922);
        end = System.currentTimeMillis();
        System.out.println(result);
        System.out.println("陣列耗時:" + (end - start));
    }

    /**
     * 陣列約瑟夫環
     */
    public static int yuesefuhuan_arr(int n, int m) {
        int size = n;
        ArrayList<Integer> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            list.add(i);
        }
        int index = 0;
        while (size > 1) {
            //取模可以回到起點
            index = (index + m - 1) % size;
            list.remove(index);
            size--;
        }
        return list.get(0);
    }


    /**
     * 迴圈連結串列約瑟夫環力扣超時
     * @param n
     * @param m
     * @return
     */
    public static int yuesefuhuan_link(int n, int m) {
        if (n == 1) {
            return n - 1;
        }

        Node<Integer> headNode = new Node<>(0);
        Node<Integer> currentNode = headNode;
        //尾結點
        Node<Integer> tailNode = headNode;

        for (int i = 1; i < n; i++) {
            Node<Integer> next = new Node<>(i);
            currentNode.next = next;
            currentNode = next;
            tailNode = currentNode;

        }
        //成環
        tailNode.next = headNode;

        //保證第一次進去的時候指向頭結點
        Node<Integer> remove = tailNode;
        Node<Integer> preNode = tailNode;
        int counter = n;
        while (true) {
            for (int i = 0; i < m; i++) {
                //一直移除頭結點則,前置結點不動
                if (m != 1) {
                    preNode = remove;
                }
                remove = remove.next;
            }
            preNode.next = remove.next;
            if (--counter == 1) {
                return preNode.value;
            }
        }
    }

    static class Node<E>{
        E value;
        Node next;
        public Node() {
        }
        public Node(E value) {
            this.value = value;
        }
    }
}

 

執行之後看下結果對比,我的機器CPU還算可以I7-8700,記憶體16G,結果都是一樣的說明我們的兩種演算法都是正確的,但是耗時的差別就很大很大了

 

 

連結串列耗時30多秒,陣列耗時86毫秒,,差不多400倍的差距。

之前也說到資料在記憶體中是連續的,可以藉助CPU的快取機制預讀資料,而連結串列每次還需要根據指標去尋找。其次就是兩種方式時間複雜度是不一樣的,我們上面的用的資料70866, 116922。O(n*m)和O(n)差距有多大。所以說不同演算法對程式的效能影響還是很大的,這應該就是體現了“演算法之美”了吧。提到這個問題,最先想到的可能就是迴圈連結串列來解決,最後卻發現,迴圈連結串列並不是一個很好的解決方式。這就像我們平時寫程式碼,需求下來的時候想著怎樣怎樣去實現,但是最後上線版本中,肯定改了又改的版本,有些方案可能整體換血都可能。再者就是從這個問題中就看出了演算法的重要性。當然在力扣上面還有一種反推法實現的,比陣列實現的程式碼更少,效能更高,感興趣的自己搜,這裡就不列出來對比了。

&n