資料結構之php實現單向連結串列

thinkabel發表於2021-01-15

圖片

在前面的文章中, 我們一起學習了, 陣列, 棧, 佇列. 他們有共同的特點, 線性資料結構。我們實現他們,都是依託於(靜態)陣列, 靠resize來解決固定容量問題(在PHP中體現不出靜態陣列和動態陣列的區別,放到java中,就能深有體會了),因為底層通過陣列實現,在儲存過程中都是有序的進行儲存。今天我們一起學習的連結串列也是一種線性的資料結構,但是在儲存的時候,是無序的。

我們帶著幾個問題往下進行學習

  • 陣列和連結串列它們之間相同點,不同點是什麼?

  • 有了陣列為什麼還要有連結串列?

一、概念

連結串列(Linked list)是一種常見的基礎資料結構,是一種線性表,但是並不會按線性的順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer)。由於不必須按順序儲存,連結串列在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查詢一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)。

維基百科

通過定義我們能看到,連結串列是最常見的一種基礎的,儲存的是無序的一種線性資料結構。

圖片

(單向連結串列的示意圖)

連結串列中最簡單一種是單向連結串列, 或叫做單連結串列, 它包含兩個域和一個指標域, 指標域用於指向下一個節點資訊, 而最後一個節點則指向一個空值, 如圖所示:

圖片

(連結串列域的示意圖)

單向連結串列的節點通常由兩個部分構成,一個是節點儲存的值e, 另一個就是節點的指標next. 連結串列 與 陣列類似, 也可以進行 查詢, 插入, 刪除, 讀取等操作, 但是由於連結串列與陣列的特性不同, 導致不同操作的複雜度也不同。

二、操作流程圖分析

在學習連結串列這一章節,有時候覺得會特別的繞,希望大家不要總是盯著程式碼,在腦子裡面想。儘量使用圖,圖是最容理解的,也會很清晰的看到是怎麼個流程。

** 1. 新增操作**

我們先來看下 往 連結串列頭部新增元素

圖片

(連結串列頭部新增元素)

現在我們想往頭部新增元素, 就需要把當前 node 的下一個節點指向 當前 head , 插入進去後, 頭部節點就變成了 當前插入的 node. 我們維護一下連結串列長度, 程式碼即是:

 node.next = head

圖片

(插入成功後的示意圖)

在連結串列頭插入是最簡單的, 只需要一次操作就可以插入成功。我們看下在連結串列任意位置插入,又是怎麼操作的呢?

比如我們在“**索引”為2的地方新增元素 888 (在連結串列中沒有索引的概念, 我們只是延伸一下陣列的概念,方便我們進行手動定位位置*), *第一步,我們需要建立出 888 這個節點, 然後需要找到 索引為 2 的上一個節點, 然後把 上一個節點的 next 賦值 給 當前插入節點的 next, 然後再把 上一個節點的next 指向 當前節點。**(這裡的順序一定不能反了,如果說先把上一個節點的next 指向當前節點, 再把上一個節點的next 賦值給當前插入節點的next, 大家仔細想一下, 是不是 都指向 當前的 節點, 連結串列就斷了), 所以這裡大家一定不要寫反了。

我們來看下流程圖是怎樣的?

圖片

(建立節點和定義prev示意圖)

這個時候,我們建立了一個 888節點 node,然後定義了 上一個節點 prev , 這時候我們遍歷迴圈連結串列到上一個節點。

圖片

(遍歷節點到上一個節點示意圖)

然後我們將上一個節點的 next 節點資訊, 給 當前節點的 next , 然後 上節點的 next 指向 當前節點,維護連結串列長度, 看圖:

圖片

(連結串列交換示意圖)

程式碼實現也就是:

node.next = prev.next

圖片

(插入成功示意圖)

*現在我們思考一下, 如果插入在 頭部的話, 怎麼辦? *頭部並沒有上一個節點可以供我們使用, 所以我們這個時候, 有兩種處理方法:

1、我們在插入首位的時候,做個特殊處理,利用上面插入首位的方法進行插入。

2、我們可以設立一個虛擬節點來實現,這樣首位也是有前一個節點了。就想我們在學習迴圈佇列時,有意的浪費掉一個空間,這個原理也是一樣的。

第一種方式我就不進行畫圖了,我們直接看第二種設立虛擬頭結點(dummyHead),先看下圖:

圖片

(虛擬頭結點示意圖)

  • 引入虛擬頭節點dummyHead使得 LinkedList 成了只包含虛擬頭節點的資料結構,在初始化 LinkedList 的時候,為dumyHead分配了空間(不像 head指標在初始化的時候沒有分配空間,只是指向 null),此時 dumyHead中,dumyHead.e = nulldumyHead.next = null

  • 由於虛擬頭節點的引入,使得向 LinkedList 的指定位置新增元素時,無需針對在隊首插入元素的情況做特殊處理;

  • 由於連結串列的插入需要找到插入節點的前驅節點,因此遊標 prev 要停在插入位置的前一個位置;初始時,將 prev 指向 dumyHead,當 i = 0 時,prev 向右移動一格,prev 指向索引為 0 的節點,即第一個元素(dumyHead 是第一個節點的前驅節點),所以 i = index - 1 時,prev 也指向了 index - 1 ,即:插入位置的前一個位置,從而找到了插入節點的前驅節點;

** 2. 修改操作**

遍歷迴圈連結串列, 頭部節點 應該是 dummyHead->next, 才是正式節點。通過 傳遞 的 索引, 進行修改 需要修改的元素, 這個比較簡單,只是替換 node 節點中的 元素資訊.

** 3. 刪除操作**

首先我們也是和新增操作一樣, 需要找到要刪除元素的前一個節點, 看下圖:

圖片

(找到要刪除的節點 和 上一個 節點)

然後將前一個節點的 next 指向 要刪除節點的 next 節點,這個時候, 要刪除的節點已經斷開連線, 然後把要刪除的next 節點 指向 null, 等垃圾回收機制刪除這個節點就好, 我們維護 連結串列 長度. 看下圖:

圖片

(找到要刪除的節點 和 上一個 節點)

三、程式碼實現
LinkedList.php

<?php
/**
 * Created by : PhpStorm
 * User: think abel
 * Date: 2021/1/11 0011
 * Time: 22:12
 */

class LinkedList
{
    // 連結串列虛擬頭結點
    private $dummyHead;

    // 連結串列的元素數
    private $size;

    /**
     * LinkedList constructor.
     */
    public function __construct()
    {
        $this->dummyHead = new Node(null, null);
        $this->size      = 0;

    }

    /**
     * Notes: 獲取連結串列中的元素個數
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 22:28
     *
     * @return int
     */
    public function getSize(): int
    {
        return $this->size;

    }

    /**
     * Notes: 連結串列是否為空
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 22:28
     *
     * @return bool
     */
    public function isEmpty(): bool
    {
        return $this->size == 0;

    }

    /**
     * Notes: 在連結串列的第 index 處 新增新的元素 e
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 22:45
     *
     * @param $index
     * @param $e
     *
     * @return string
     */
    public function add($index, $e)
    {
        try {
            if ($index < 0 || $index > $this->size) {
                throw new Exception("新增失敗, index require >= 0 and <= " . $this->size);
            }

            /**
             * @var Node $prev 為虛擬頭結點, 所以遍歷 輸入的 index 次 就是前一個節點
             */
            $prev = $this->dummyHead;
            for ($i = 0; $i < $index; $i ++) {
                $prev = $prev->next;
            }

            /**
             * 通過 當前傳遞的 資料 和 上一個節點所指向的下一個節點資訊 來建立 當前 節點
             * 並把 上一個節點 所指向的下一個節點資訊 變為當前建立的節點
             */
            $prev->next = new Node($e, $prev->next);
            $this->size ++;
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 在連結串列頭新增新的元素 e
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 22:34
     *
     * @param $e
     */
    public function addFirst($e)
    {
        $this->add(0, $e);

    }

    /**
     * Notes: 向連結串列末尾新增元素 e
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 23:23
     */
    public function addLast($e)
    {
        $this->add($this->size, $e);

    }

    /**
     * Notes: 獲取第 index 位置的 元素
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 23:36
     *
     * @param int $index
     *
     * @return string
     */
    public function get(int $index)
    {
        try {
            if ($index < 0 || $index > $this->size) {
                throw new Exception("新增失敗, index require >= 0 and <= " . $this->size);
            }

            /**
             * @var Node $cur 當前節點的下一個節點, 因為前面有一個虛擬節點
             */
            $cur = $this->dummyHead->next;
            for ($i = 0; $i < $index; $i ++) {
                $cur = $cur->next;
            }

            return $cur->e;
        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 獲取第一個元素
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 23:40
     *
     * @return string
     */
    public function getFirst()
    {
        return $this->get(0);

    }

    /**
     * Notes: 獲取最後一個元素
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 23:40
     *
     * @return string
     */
    public function getLast()
    {
        return $this->get($this->size - 1);

    }

    /**
     * Notes: 更新一個元素
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 23:44
     *
     * @param int $index
     * @param     $e
     *
     * @return string
     */
    public function set(int $index, $e)
    {
        try {
            if ($index < 0 || $index > $this->size) {
                throw new Exception("修改失敗, index require >= 0 and <= " . $this->size);
            }

            /**
             * @var Node $cur 當前節點的下一個節點, 因為前面有一個虛擬節點
             */
            $cur = $this->dummyHead->next;
            for ($i = 0; $i < $index; $i ++) {
                $cur = $cur->next;
            }

            $cur->e = $e;

        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 查詢元素是否存在
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 23:49
     *
     * @param $e
     *
     * @return bool
     */
    public function contains($e): bool
    {
        /**
         * @var Node $cur
         */
        $cur = $this->dummyHead->next;

        /**
         * 不知道遍歷多少次, 使用 while 迴圈
         * 如果 當前的元素 == $e 說明 存在, 返回true
         * 否則 將 當前的下一個元素 賦值給 當前元素 繼續遍歷
         * 如果 都沒有, 說明不存在, 返回 false
         */
        while ($cur != null) {
            if ($cur->e == $e) {
                return true;
            }

            $cur = $cur->next;
        }

        return false;

    }

    /**
     * Notes: 刪除 index 位置的元素
     * User: think abel
     * Date: 2021/1/12 0012
     * Time: 0:15
     *
     * @param int $index
     *
     * @return string|null
     */
    public function remove(int $index)
    {
        try {
            if ($index < 0 || $index > $this->size) {
                throw new Exception("刪除失敗, index require >= 0 and <= " . $this->size);
            }

            /**
             * @var Node $prev 當前節點的下一個節點, 因為前面有一個虛擬節點
             */
            $prev = $this->dummyHead;
            for ($i = 0; $i < $index; $i ++) {
                $prev = $prev->next;
            }

            /**
             * @var Node $removeNode
             */
            $removeNode = $prev->next;
            $prev->next = $removeNode->next;

            $removeElement = $removeNode->e;
            $removeNode    = null;
            $this->size --;

            return $removeElement . PHP_EOL;


        }
        catch (Exception $e) {
            return $e->getMessage();
        }

    }

    /**
     * Notes: 刪除第一個元素
     * User: think abel
     * Date: 2021/1/12 0012
     * Time: 0:16
     *
     * @return string|null
     */
    public function removeFirst()
    {
        return $this->remove(0);
    }

    /**
     * Notes: 刪除最後一個元素
     * User: think abel
     * Date: 2021/1/12 0012
     * Time: 0:16
     *
     * @return string|null
     */
    public function removeLast()
    {
        return $this->remove($this->size - 1);
    }

    /**
     * Notes: 列印輸出
     * User: think abel
     * Date: 2021/1/11 0011
     * Time: 23:52
     *
     * @return string
     */
    public function toString(): string
    {
        /**
         * @var Node $cur
         */
        $cur = $this->dummyHead->next;

        $str = '';
        for ($cur; $cur != null; $cur = $cur->next) {
            $str .= $cur->e . '-->';
        }

        $str .= 'NULL';
        return $str . PHP_EOL;

    }
}

/**
 * 建立節點類
 * Class Node
 */
class Node
{
    // 節點元素
    public $e;

    // 指向下一個節點資訊
    public $next;

    /**
     * Node constructor.
     *
     * @param null $e
     * @param null $next
     */
    public function __construct($e = null, $next = null)
    {
        $this->e    = $e;
        $this->next = $next;

    }

}

index.php

<?php

include "LinkedList.php";
$linkedList = new LinkedList();
for ($i = 0; $i < 10; $i++) {
    $linkedList->addFirst($i);
    echo $linkedList->toString();
}

$linkedList->add(2, 666);
echo $linkedList->removeLast();
echo $linkedList->toString();

四、簡單複雜度分析

O: 描述是演算法的執行時間 和 輸入資料之間的關係 — 程式執行時間 和 數資料 成線性關係 O(n)

  1. 新增操作 O(N)
    addLast           O(N)
    addFirst          O(1)
    add(index, e)     O(N/2) = O(N)
  2. 修改操作 O(N)
    set(index, e)     O(N)
  3. 刪除操作 O(N)
    removeLast(e)     O(N)
    removeFirst(e)    O(1)
    remove(index, e)  O(N/2) = O(N)
  4. 查詢操作 O(N)
    get(index)     O(N)
    contains(e)    O(N)
    我們看到, 增刪改的操作都是O(N), 但是想一下, 我們只對連結串列頭進行操作的話, 複雜度就是O(1)

五、總結

連結串列的一些基礎操作已經學習完了, 現在將文章開始的幾個問題進行一下總結回答.
1、陣列和連結串列它們之間相同點,不同點是什麼?
—相同點:都是線性 資料結構
—不同點:儲存的順序是不相同的, 陣列是有序的,連結串列是無序的。

2、有了陣列為什麼還要有連結串列?
連結串列和陣列,他倆是各有千秋,畢竟什麼都是雙面性的。
陣列的特點在於隨機訪問性強, 查詢速度快。缺點在於插入和刪除時效率低,可能會存在浪費記憶體的情況,記憶體要求高,必須要有足夠的連續記憶體空間。
連結串列的特點在於插入刪除效率高,記憶體利用率高,不會浪費記憶體,大小沒有固定,容易擴充。但是連結串列 在讀取時效率慢。
所以他倆是進行互補的狀態

最後祝大家 工作開心, 永不加班~ 加油……

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章