在前面的文章中, 我們一起學習了, 陣列, 棧, 佇列. 他們有共同的特點, 線性資料結構。我們實現他們,都是依託於(靜態)陣列, 靠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 = null
,dumyHead.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)
- 新增操作 O(N)
addLast O(N) addFirst O(1) add(index, e) O(N/2) = O(N)
- 修改操作 O(N)
set(index, e) O(N)
- 刪除操作 O(N)
removeLast(e) O(N) removeFirst(e) O(1) remove(index, e) O(N/2) = O(N)
- 查詢操作 O(N)
我們看到, 增刪改的操作都是O(N), 但是想一下, 我們只對連結串列頭進行操作的話, 複雜度就是O(1)get(index) O(N) contains(e) O(N)
五、總結
連結串列的一些基礎操作已經學習完了, 現在將文章開始的幾個問題進行一下總結回答.
1、陣列和連結串列它們之間相同點,不同點是什麼?
—相同點:都是線性 資料結構
—不同點:儲存的順序是不相同的, 陣列是有序的,連結串列是無序的。
2、有了陣列為什麼還要有連結串列?
連結串列和陣列,他倆是各有千秋,畢竟什麼都是雙面性的。
陣列的特點在於隨機訪問性強, 查詢速度快。缺點在於插入和刪除時效率低,可能會存在浪費記憶體的情況,記憶體要求高,必須要有足夠的連續記憶體空間。
連結串列的特點在於插入刪除效率高,記憶體利用率高,不會浪費記憶體,大小沒有固定,容易擴充。但是連結串列 在讀取時效率慢。
所以他倆是進行互補的狀態
最後祝大家 工作開心, 永不加班~ 加油……
本作品採用《CC 協議》,轉載必須註明作者和本文連結