Algorithms_基础数据结构(02)_线性表之链表_单向链表
文章目錄
- 大綱圖
- 鏈表的經典面試題目
- 如何設計一個LRU緩存淘汰算法
- 約瑟夫問題
- 順序表VS 鏈表
- 鏈表的定義
- 鏈表的特點
- 常見的鏈表結
- 單向鏈表
- 單向鏈表的查找
- 單向鏈表的插入
- 頭插
- 尾部插入
- 中間插入
- 單向鏈表的刪除
- 刪除頭節點
- 刪除中間的節點
- 刪除尾部節點
- Code
- 總結
大綱圖
鏈表的經典面試題目
如何設計一個LRU緩存淘汰算法
tip:單向鏈表
約瑟夫問題
N個人圍成一圈,從第一個開始報數,第M個將被殺掉,最后剩下一個,其余人都將被殺掉。
舉個例子: 假設N=6,M=5,被殺掉的順序是:5,4,6,2,3,1。
現在問你最后留下的人是誰?
比如N=6,M=5 ,留下的就是1
tips: 單向循環鏈表
順序表VS 鏈表
鏈表的定義
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。
鏈表由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態生成。
每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。 比如下面這種
相比于線性表順序結構,操作復雜。由于不必須按順序存儲,鏈表在插入的時候可以達到O(1)的復雜度,比另一種線性表順序表快得多,但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而線性表和順序表相應的時間復雜度分別是O(logn)和O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由于增加了結點的指針域,空間開銷比較大。
鏈表的特點
- 不需要連續的內存空間
- 有指針引用
常見的鏈表結
三種最常見的鏈表結構:單向鏈表、雙向鏈表 和循環鏈表 (單向循環鏈表、雙向循環鏈表)
單向鏈表
單向鏈表是由一個個節點組成的,每個節點是一種信息集合,包含元素本身以及下一個節點的地址。
從單鏈表圖中,可以發現,有兩個結點是比較特殊的,它們分別是第一個結點和最后一個結點。
我們一般把第一個結點叫作頭結點,把最后一個結點叫作尾結點。
其中,頭結點用來記錄鏈表的基地址。有了它,我們就可以遍歷得到整條鏈表。
而尾結點特殊的地方是:指針不是指向下一個結點,而是指向一個空地址NULL,表示這是鏈表上最后一個結點。
單向鏈表的查找
沒啥好說的,從頭結點開始遍歷,直到找到停止,不存在的話,就是全部遍歷了,查找的時間復雜度為O(n)
code如下
/*** 根據值,找到對應的節點* 從頭節點 開始遍歷** @param data* @return*public ArtisanNode find(Object data) {// 頭節點的臨時變量,直接用head就把head給改變了,不可取。ArtisanNode currentNode = head;// 遍歷while (currentNode != null) {if (currentNode.data == data) { // 如果匹配,終止循環break;} else {currentNode = currentNode.next; // 不匹配則將下個節點賦值給當前節點,繼續循環}}System.out.println("當前節點:" + currentNode.toString());return currentNode;}單向鏈表的插入
插入的話 分為三種場景,頭插、尾插、中間插入
頭插
頭插就是插入頭部節點。 流程如下
尾部插入
尾插就是插入尾部節點。 流程如下
中間插入
單向鏈表的刪除
刪除頭節點
刪除中間的節點
刪除尾部節點
Code
/*** @author 小工匠* @version v1.0* @create 2020-01-01 07:36* @motto show me the code ,change the word* @blog https://artisan.blog.csdn.net/* @description**/public class ArtisanSingleLinkedList {// 頭節點private ArtisanNode head;// 單向鏈表的長度private int size;/*** 構造函數*/public ArtisanSingleLinkedList() {this.size = 0; // 初始化長度為0this.head = null;// 初始化head為null}/*** 頭插** @param data*/public void add2Head(Object data) {ArtisanNode node = new ArtisanNode(data); // 初始化一個Nodenode.next = head;// 將這個新Node的next指向headthis.head = node;// 把這個新的node置為headsize++; // 更新鏈表容量}/*** 尾插** @param data*/public void add2Tail(Object data) {ArtisanNode node = new ArtisanNode(data);ArtisanNode currentNode = head;while (currentNode != null){ // 遍歷if (currentNode.next == null){ // next為null,說明到了tailcurrentNode.next = node; // 將next節點指向新增節點break; // 跳出循環}else {currentNode = currentNode.next; // 不匹配則將下個節點賦值給當前節點,繼續循環}}size++;}/*** 插入鏈表的中間 假設在第N個位置插入** @param data*/public ArtisanNode add2Nth(Object data, int position) {ArtisanNode node = new ArtisanNode(data);if (position == 0) { // 如果position = 0 ,頭插add2Head(data);} else { // 找到對應的位置// 頭節點的臨時變量,直接用head就把head給改變了,不可取。ArtisanNode currentNode = head;for (int i = 1; i < position; i++) {currentNode = currentNode.next; // 一直往后遍歷}node.next = currentNode.next; // 當前節點的next節點 賦值給 新節點的nextcurrentNode.next = node; // 當前節點的next節點指向新節點}size++; // 更新鏈表容量return node;}/*** 根據值,找到對應的節點* 從頭節點 開始遍歷** @param data* @return*/public ArtisanNode find(Object data) {// 頭節點的臨時變量,直接用head就把head給改變了,不可取。ArtisanNode currentNode = head;// 遍歷while (currentNode != null) {if (currentNode.data == data) { // 如果匹配,終止循環break;} else {currentNode = currentNode.next; // 不匹配則將下個節點賦值給當前節點,繼續循環}}System.out.println("當前節點:" + currentNode.toString());return currentNode;}/*** @return 單向鏈表當前的容量*/public int getSize() {System.out.println("ArtisanSingleLinkedList 當前的容量為:" + size);return size;}/*** 刪除頭節點* 時間復雜度 O(1)*/public ArtisanNode deleteHead(){this.head = head.next ; // 將頭節點的nextsize--;return head;// 返回頭節點}/*** 刪除指定位置的節點** 時間復雜度 O(n)*/public ArtisanNode deleteNth(int position){ArtisanNode currentNode = head;if (position == 0 ){deleteHead();}else{for (int i = 1; i < position; i++) {currentNode = currentNode.next;}currentNode.next = currentNode.next.next; //cur.next 表示的是刪除的點,后一個next就是我們要指向的}size--;return currentNode.next; // 返回被移除的節點}/*** 刪除尾部節點*/public ArtisanNode deleteTail(){ArtisanNode currentNode = head;ArtisanNode previosNode = head;while (currentNode != null){if (currentNode.next == null){previosNode.next = null;break;}else {previosNode = currentNode;currentNode = currentNode.next;}}size--;return previosNode; // 返回尾結點}public void print(){ArtisanNode currentNode = head;while(currentNode != null){System.out.print(currentNode.data+" -> ");// 從頭節點開始輸出currentNode = currentNode.next;}System.out.println();}public static void main(String[] args) {ArtisanSingleLinkedList single = new ArtisanSingleLinkedList();// 頭插single.add2Head(5);single.add2Head(4);single.add2Head(3);single.add2Head(2);single.add2Head(1);single.getSize();single.print();// 查找數據single.find(4);// 指定位置插入single.add2Nth("InsertedData", 2);single.getSize();single.print();// 尾插single.add2Tail("小工匠");single.print();single.getSize();// 刪除中間節點single.deleteNth(2);single.print();// 刪除尾部節點single.deleteTail();single.getSize();single.print();} }/*** Node節點*/ class ArtisanNode {Object data; // 數據域ArtisanNode next; // 指針域,指向下一個節點/*** 構造函數** @param data*/public ArtisanNode(Object data) {this.data = data;}@Overridepublic String toString() {return "ArtisanNode{" +"data=" + data +", next=" + next +'}';} }總結
-
1. 說到數組就要想到下標,查找使用下標去查找,因為根據下標查找的時間復雜度為O(1).
不要試圖使用根據元素的值去查找,因為這樣的話,根據值去查找,時間復雜度為O(n) -
2. 數組的話,需要考慮數組越界的問題, 合理的動態擴容/縮容 , 減少不必要的內存浪費
-
3. CPU可以把數組緩存到CPU內部,執行效率比鏈表更高。 為啥可以緩存數組呢? ---->
數組是在內存里連續的, 索引找到了首地址,其他元素也就都找到了。 鏈表在內存中 是沒有規律的,通過指針項鏈,沒發被CPU緩存。 -
4. 說到鏈表,不需要考慮下標的問題,所以不能根據下邊來查找,肯定是需要根據數據域存儲的數據來查找,從頭遍歷,通過next執行,一直遍歷下去,直到找到數據位置。所以時間復雜度為O(n)
-
5. 鏈表 雖然查找慢,但是插入和刪除快啊,動動指針即可,不用挪元素。時間復雜度O(1)
-
6. 鏈表 因為本身支持動態擴容,所以一定要考慮 鏈表占用的內存大小。。。切記。
總結
以上是生活随笔為你收集整理的Algorithms_基础数据结构(02)_线性表之链表_单向链表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Algorithms_基础数据结构(01
- 下一篇: CPU_X86架构和ARM架构入门篇