連結串列的基本操作和高頻演算法題
連結串列的基本操作
連結串列的基礎操作有查詢、刪除、新增。
查詢
先定義一下連結串列的資料結構:
class DataNode{ int key; int value; DataNode pre; DataNode next; public DataNode(){}; public DataNode (int key,int value){ this.key = key; this.value = value; } }
其中的key和value就是節點實際儲存的值。pre和next分別指向前一個節點和下一個節點。一般的單向連結串列只有next,我這裡定義的是雙向連結串列。查詢操作就是從頭節點,一直遍歷next,直到找到目標節點為止。可以用while迴圈或者遞迴實現,找到目標節點就跳出或返回即可,時間複雜度為O(n)。
刪除
以上面的雙向連結串列為例,演示一下刪除操作。我們假設有三個節點A、B、C,現要刪除B節點。把A.next指向C,C.pre指向A。中間的B節點就不在鏈路上了,會被垃圾收回器給回收掉。
public void delNode(DataNode node){ node.pre.next = node.next; node.next.pre = node.pre; }
上面的程式碼初看可能有點繞,其實連結串列除了查詢操作,都有點繞,建議畫圖理解。其中 node.pre.next
就是上一個節點的下一個節點,把它改成 node.next
,就相當於讓上一個節點指向自己的下一個節點。第二行程式碼就是讓下一個節點的pre指向自己的上一個節點。刪除操作的時間複雜度為O(1)。
新增
假設有A、C兩個節點,現要往中間新增一個B節點。思路看圖都能想到,你的寫法不一定要和我一樣,只是注意別丟失節點了,程式碼如下:
//寫法1 public void addNode(DataNode pre,DataNode node){ //先記錄一下pre.next節點,否則下一步會丟失C節點 DataNode next = pre.next; // 記錄C pre.next = node; //A->B node.next = next; //B->C next.pre = node; // B<-C node.pre = pre; // A<-B } //寫法2,不用臨時變數 public void addNode(DataNode pre,DataNode node){ node.next = pre.next; //B->C node.pre = pre; //A<-B pre.next = node; // A->B node.next.pre = node; //C<-B }
演算法題
LRU快取
關於連結串列的演算法題中,我覺得最能訓練連結串列操作的就是LRU快取。即給出已給固定容量的容器,往裡put元素時,如果容量到達最大, 就刪除最久未使用的元素 。
題目描述:
實現 LRUCache 類:
LRUCache(int capacity) 以 正整數 作為容量 capacity 初始化 LRU 快取
int get(int key) 如果關鍵字 key 存在於快取中,則返回關鍵字的值,否則返回 -1 。
void put(int key, int value) 如果關鍵字 key 已經存在,則變更其資料值 value ;如果不存在,則向快取中插入該組 key-value 。如果插入操作導致關鍵字數量超過 capacity ,則應該 逐出 最久未使用的關鍵字。
函式 get 和 put 必須以 O(1) 的平均時間複雜度執行。
思路:根據key找到value,所以肯定要一個Hash表儲存值。元素數量超過capacity就要刪除最久未使用的關鍵字。我們就設計一個連結串列,每次get元素A時,就把A移到連結串列頭部。需要刪除元素時,直接刪除連結串列尾部的元素,尾部的就是最久沒使用的。
每次get時要把對應的元素移至頭部,為了避免遍歷連結串列,設計Hash表的value型別可以設定成DataNode,這樣就免去的連結串列查詢的時間。DataNode就複用開頭定義的資料結構。
class LRUCache { int capacity; HashMap<Integer,DataNode> map; //定義一個虛擬的頭節點和尾節點,方便刪除尾節點和往頭節點新增元素 DataNode head; DataNode tail; //1.初始化相關屬性 public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(); head = new DataNode(); tail = new DataNode(); head.next = tail; tail.pre = head; } //2.實現get邏輯,裡面的moveToHead可以先不實現 public int get(int key) { DataNode node = map.get(key); if(node==null){ return -1; } //把node節點移至頭部 moveToHead(node); return node.value; } //3.實現put邏輯 public void put(int key, int value) { if(map.containsKey(key)){ //如果當前key已經存在 DataNode node = map.get(key); moveToHead(node); node.value = value; }else{ //不存在就新建一個node,如果超過capacity就刪除尾部節點 DataNode node = new DataNode(key,value); map.put(key,node); if(map.size()>capacity){ //因為還要從map中刪除元素,所以removeTail要有返回值 DataNode delNode = removeTail(); map.remove(delNode.key); } addHead(node); } } //4.最後一步,實現上面所需的連結串列操作方法 private void moveToHead(DataNode node){ //先刪除,再移至頭部 removeNode(node); addHead(node); } private void addHead(DataNode node){ node.next = head.next; node.pre = head; head.next = node; node.next.pre = node; } private DataNode removeTail(){ DataNode delNode = tail.pre; removeNode(delNode); return delNode; } //removeTail和moveToHead都有刪除元素的操作,所以再提取一個刪除方法 private void removeNode(DataNode node){ node.pre.next = node.next; node.next.pre = node.pre; } }
反轉連結串列
反轉連結串列也是面試中出現頻率比較高的,反轉連結串列是個單向連結串列,它的操作比雙向連結串列更簡單。
題目描述:
給你單鏈表的頭節點 head ,請你反轉連結串列,並返回反轉後的連結串列。
思路:定義兩個指標(變數),一個指向當前節點,一個指向前一個節點。每次反轉指標指向的兩個節點,然後指標往後移一位。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseList(ListNode head) { if(head==null||head.next==null){ return head; } ListNode pre = null; ListNode node = head; while(node!=null){ ListNode temp = node.next; //反轉node和pre node.next = pre; //node和pre往後移一位 pre = node; node = temp; } //因為node最終會移到尾節點的next上,也就是null //所以pre才是真正的尾節點,也就是反轉後的頭節點 return pre; } }
環形連結串列
題目描述:
出一個連結串列的head,判斷該連結串列是否是環形連結串列。如果是,就返回環形的入口。如果不是,就返回null。
如上圖,入口節點就是2。
思路1:要做出這個題不難,第一下就能想到: 邊遍歷連結串列,邊往Hash表儲存節點 ,每次遍歷前判斷Hash表是否存在當前節點,如果存在,這個節點就是環形入口。如果遍歷完了,還沒有重複節點,就說明沒有環形。時間複雜度:O(n),空間複雜度:O(n)。
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode detectCycle(ListNode head) { HashSet<ListNode> set = new HashSet<>(); while(head!=null){ if(set.contains(head)){ return head; } set.add(head); head = head.next; } return null; } }
思路2:優化連結串列的空間複雜度常用手段就是用指標,思路2就是定義兩個快慢指標,屬於數學邏輯範疇了。我們定義一個慢指標slow,一個快指標fast。slow一次移動一位,fast一次移動兩位。
-
如果fast移到了null節點,說明連結串列無環,直接返回null
-
fast和slow相遇
-
- 此時fast和slow一定在環形內,否則不可能相遇。 我們假設head到環形入口(不含入口)的長度為x,環形長度為y 。
- 然後假設slow走了s步,則fast走了2s步(fast是slow的兩倍速)
- fast和slow相遇時,fast 在環內 比slow 多走 了ny步(關鍵點,可以畫圖理解一下)
-
-
- 所以fast=2s=s+ny(s是slow走的步數,ny是fast比slow多走的步數)
- 所以s=ny
-
-
- 根據上面的推測s=ny,接著可以推算出, 入口點就是x+ny 。因為y是環形長度,n是正整數,所以ny實際上和y沒區別,無非就是多繞了幾圈。(關鍵點,也可以畫圖理解一下)。
- 此時slow已經走了ny步,所以再走x步就是入口點了。但是我們不知道x等於多少,那我們就讓一個指標從head再走一遍,一次走一步,和slow相遇點就是入口點。為了少建立一個變數,可以讓fast指標回到head節點重新走。
public class Solution { public ListNode detectCycle(ListNode head) { if(head==null||head.next==null){ return null; } ListNode slow = head; ListNode fast = head; while(true){ if(fast == null||fast.next == null){ return null; } slow = slow.next; fast = fast.next.next; //第一次相遇 if(fast==slow){ break; } } fast = head; while(fast!=slow){ fast = fast.next; slow = slow.next; } return slow; } }
總結
連結串列必須要掌握它的刪除、新增、查詢三個基礎操作。連結串列的型別還分為:單向連結串列、迴圈連結串列(頭尾相連,或者帶環的)、雙向連結串列。只要掌握了雙向連結串列的基礎操作,其他連結串列都不在話下。
關於連結串列的演算法中,因為不能像陣列那樣,通過下標隨機訪問,所以一般會把節點存進Hash表。如果Hash表也不想存,想優化空間複雜度,一般的做法是定義指標。單向連結串列一般要定義雙指標,一個指向當前,一個指向前一個,如果是雙向連結串列,只用定義一個。但是在演算法題中,單純只考連結串列的題目比較少,很多都會帶一些其他知識點。比如連結串列的排序、連結串列的二分查詢等。只要熟練掌握連結串列的插入、刪除,只用考慮排序、查詢的邏輯就行了,跟陣列的排序、二分查詢沒啥區別。
- 分享自己平時使用的socket多客戶端通訊的程式碼技術點和軟體使用
- iNeuOS工業網際網路作業系統,增加2154個檢視建模(WEB組態)行業向量圖元、大屏背景及相關圖元
- 多臺雲伺服器的 Kubernetes 叢集搭建
- Elasticsearch學習系列四(聚合搜尋)
- 關於swiper外掛在vue2的使用
- 使用 Abp.Zero 搭建第三方登入模組(一):原理篇
- LVGL庫入門教程 - 顏色和影象
- 物聯網?快來看 Arduino 上雲啦
- SpringBoot JWT Redis 開源知識社群系統
- CVPR2022 | 可精簡域適應
- Spring框架系列(3) - 深入淺出Spring核心之控制反轉(IOC)
- 面試突擊59:一個表中可以有多個自增列嗎?
- CVPR2022 | 弱監督多標籤分類中的損失問題
- JDBC、ORM、JPA、Spring Data JPA,傻傻分不清楚?一文帶你釐清箇中曲直,給你個選擇SpringDataJPA的...
- Spring Security:使用者和Spring應用之間的安全屏障
- Mybatisi和Spring整合原始碼分析
- 前端學習 linux —— 第一篇
- call apply bind的作用及區別? 應用場景?
- Bika LIMS 開源LIMS集——實驗室檢驗流程概述及主頁、面板
- 軟體專案管理 7.5.專案進度模型(SPSP)