鏈表的原理及Js基本實現
虛心接受批評和指正,互相成就,共勉!
從這篇文章你會收穫什麼?
- 瞭解單鏈表和雙鏈表的基本結構;
- 在單鏈表或雙鏈表中實現遍歷、插入和刪除;
- 在鏈表中使用雙指針技巧(快慢指針技巧);
- 解決一些經典問題,例如反轉鏈表;
目錄
鏈表的由來
一、鏈表的由來
我們接觸最多的數據存儲結構應該是數組了,在實際場景中它的出現頻率極高,但是它並不能適於用所有情況。這也是的鏈表
原因如下:
- 在很多編程語言中,數組的長度是固定的,所以當數組已被數據填滿時,再要加入新的元素就會非常困難。
- 在數組中的
添加
和刪除
元素很麻煩,因爲需要將數組中的其他元素向前或向後平移。 JavaScript中
數組的主要問題是,它們被實現成了對象,與其他語言(比如 C++ 和 Java) 的數組相比,效率較低。
爲了解決上述問題
如果你發現數組在實際使用時很慢,就可以考慮使用鏈表來替代它。除了對數據的隨機訪問,鏈表幾乎可以用在任何可以使用一維數組的情況中,如果需要頻繁的刪除和添加操作,就主動考慮一下鏈表吧~
1.1 特點
優點
- 鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。
- 增加數據和刪除數據很容易。
- 鏈表中的每個元素實際上是一個單獨的對象,而所有對象都通過每個元素中的引用字段鏈接在一起。
缺點?
- 訪問時間是線性的(而且難以管道化),更快的訪問,如隨機訪問,是不可行的。與鏈表相比,數組具有更好的緩存位置。
- 失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大
鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環鏈表。鏈表可以在多種編程語言中實現。下面出現的代碼都是用Js實現的,如果不對的地方,歡迎大佬們指正,我們共勉。
二、單鏈表
單鏈表中的每個結點不僅包含值,還包含鏈接到下一個結點的引用字段。通過這種方式,單鏈表將所有結點按順序組織起來。、
下面是一個單鏈表的例子:
當你得到了head
節點,就得到了整個列表。
我們創建單一節點(Node)的操作應該是這樣的:
var Node = function(ele){
this.ele = ele; // 保存當前節點的值
this.next = null; // 保存指向下一個節點的地址
}
2.1 添加節點
就像給繩子打結一樣,添加節點,就是在兩個繩結之間,再打一個新結。
如果我們想在給定的結點 prev 之後添加新值,我們應該:
- 創建要插入的Node——cur
- 將cur節點的
next
鏈接到next節點(pre的下一個節點)
- 將pre的
next
鏈接到cur節點
在開頭添加結點
衆所周知,我們使用頭結點(head
)來代表整個列表。
因此,在列表開頭添加新節點時更新頭結點 head 至關重要。
- 初始化一個新結點
cur
; - 將新結點
cur
的next
鏈接到我們的原始頭結點 head.next節點 - 將head節點的
next
鏈接到cur
即可。
在末尾添加節點
- 創建新節點
cur
- 將鏈表的末尾節點的
next
鏈接到cur
即可
2.2 刪除節點
如果我們要刪除指定的節點cur
,該這麼做:
- 找到cur的上一個節點
prev
,及其下一個節點prev.next
(要刪除的節點) - 將
prev.next
鏈接讓 `prev.next.next,即跳過刪除節點。
注意:我們必須從頭節點遍歷至指定節點,刪除節點的平均時間複雜度是O(N)
刪除末尾節點
- 找到
next
節點鏈接爲null
的節點,以及它的前節點prev
prev.next
鏈接null
即可
三、設計鏈表
以LeetCode的中的基礎題爲例,我們嘗試用代換實現前文提過的思路。707.設計鏈表
題目
設計鏈表的實現。您可以選擇使用單鏈表或雙鏈表。單鏈表中的節點應該具有兩個屬性:val 和 next。val 是當前節點的值,next 是指向下一個節點的指針/引用。如果要使用雙向鏈表,則還需要一個屬性 prev 以指示鏈表中的上一個節點。假設鏈表中的所有節點都是 0-index 的。
在鏈表類中實現這些功能:
- get(index):獲取鏈表中第 index 個節點的值。如果索引無效,則返回-1。
- addAtHead(val):在鏈表的第一個元素之前添加一個值爲 val 的節點。插入後,新節點將成爲鏈表的第一個節點。
- addAtTail(val):將值爲 val 的節點追加到鏈表的最後一個元素。
- addAtIndex(index,val):在鏈表中的第 index 個節點之前添加值爲 val 的節點。如果 index 等於鏈表的長度,則該節點將附加到鏈表的末尾。如果 index 大於鏈表長度,則不會插入節點。如果index小於0,則在頭部插入節點。
- deleteAtIndex(index):如果索引 index 有效,則刪除鏈表中的第 index 個節點。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //鏈表變爲1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //現在鏈表是1-> 3
linkedList.get(1); //返回3
Js版代碼實現
// 創建節點類
var Node = function(ele){
this.ele = elel; // 保存當前節點的值
this.next = null; // 保存的下一個節點的地址
}
/**
* 鏈表構造函數
*/
var MyLinkedList = function() {
this.head = new Node('head')
};
/**
* 查詢 index 位節點
* @param {index} index
* @return {Node} 返回節點 || null
*/
MyLinkedList.prototype.find = function (index) {
var i = 0 // 記錄下標
var current = this.head //新節點->head節點
while(current.next){
if(i == index){
return current.next
}
i++
current = current.next
}
return null
};
/**
* 查詢 index 位節點的值
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.get = function(index) {
var _this = this
var res = _this.find(index) // 利用find查找
return res ? res.ele : -1
};
/**
* 插入到鏈表首位
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function(val) {
var current = this.head
var node = new Node(val)
node.next = current.next
current.next = node
};
/**
* 插入到鏈表首末位
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function(val) {
var current = this.head
while(current.next){
current = current.next
}
var node = new Node(val)
current.next = node
};
/**
* 在 index 位置插入值爲 val 的節點
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function(index, val) {
if(index < 0){
this.addAtHead(val) // 下標小於鏈表0,則插入頭部
return;
}
var current = this.head
var i = 0
while(current.next){
if(i == index){
var node = new Node(val)
node.next = current.next
current.next = node
return;
}
i++
current = current.next
}
this.addAtTail(val) // 下標超過鏈表長度,則插入末尾
return;
};
/**
* Delete the index-th node in the linked list, if the index is valid.
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function(index) {
if(index < 0) return;
var i = 0
var current = this.head
while(current.next){
if(i == index){
current.next = current.next.next
return;
}
i++
current = current.next
}
};
爲了方便操作,我們主動創建了一個節點爲頭節點,實際存儲過程中是完全不需要的。
四、鏈表的基本使用場景
- 對線性表的長度或者規模難以估計;
- 頻繁做插入刪除操作;
- 構建動態性比較強的線性表
鏈表的基本操作
1.創建節點
// Node類
function Node(element) {
this.element = element;
this.next = null;
}
2.創建鏈表
function LList() {
console.log('---創建---')
this.head = new Node("head");
this.find = find; //查找指定節點
this.insert = insert; //插入節點
this.remove = remove; //刪除節點
this.display = display; //打印鏈表
this.findNextEle = findNextEle; //查找存儲指定節點的節點
}
3.查找目標節點
function find(item) {
var currNode = this.head; //創建一個節點,並將頭節點指向它
while (currNode.element != item) {
currNode = currNode.next; // 如果currNode的值不是我們找的值,向下查找
}
return currNode;
}
4.添加操作
function insert(newElement, item) {
var newNode = new Node(newElement); // 創建要插入的節點
var current = this.find(item); //找到要插入的節點
newNode.next = current.next; //要添加位置節點的next -> 查找到節點的next
current.next = newNode; // 被緹娜家位置的節點
}
5.查找存儲目標節點的節點
function findNextEle(item) {
var currNode = this.head
while (!(currNode.next == null) && (currNode.next.element != item)) {
currNode = currNode.next
}
return currNode
}
6.刪除操作
function remove(item) {
console.log('---刪除---',item)
var preNode = this.findNextEle(item) //找到存儲着《待刪除節點》的節點
if (!(preNode.next == null)) {
preNode.next = preNode.next.next // 存儲着 《待刪除節點》.next -> .next.next
}
}
7.打印操作
function display() {
var currNode = this.head
while (!(currNode.next == null)) {
console.log(currNode.next.element,currNode.next.next)
currNode = currNode.next
}
}
測試數據
var testCities = new LList()
testCities.insert('北京', 'head')
testCities.insert('上海', '北京')
testCities.insert('廣州', '上海')
testCities.insert('深圳', '廣州')
testCities.display()
testCities.remove('廣州');
testCities.display()
總結
- 鏈表的特點,優缺點。
- 鏈表的插入、刪除思想
- 如何實現一個鏈表類,並且支持必要的操作
- 鏈表得實際場景的應用
關於我
- 19年畢業的前端開發
- 沉迷Js、熱衷開源(菜鳥一個)
- 郵箱:[email protected]
瞧一瞧(求Star!!!)
- LeetCode題解Javascript版本:Gitbook版本傳送門
- LeetCode題解Javascript版本:CSDN傳送門
- 前端進階筆記:Gitbook傳送門
下一篇我們會總結一下雙指針在鏈表中的作用,未完待續…