JavaScript實現單向鏈表
一、單向鏈表簡介
鏈表和數組一樣,可以用於存儲一系列的元素,但是鏈表和數組的實現機制完全不同。鏈表的每個元素由一個存儲元素本身的節點和一個指向下一個元素的引用(有的語言稱為指針或連接)組成。類似於火車頭,一節車廂載着乘客(數據),通過節點連接另一節車廂。



- head屬性指向鏈表的第一個節點;
- 鏈表中的最后一個節點指向null;
- 當鏈表中一個節點也沒有的時候,head直接指向null;
數組存在的缺點:
- 數組的創建通常需要申請一段連續的內存空間(一整塊內存),並且大小是固定的。所以當原數組不能滿足容量需求時,需要擴容(一般情況下是申請一個更大的數組,比如2倍,然后將原數組中的元素復制過去)。
- 在數組的開頭或中間位置插入數據的成本很高,需要進行大量元素的位移。
鏈表的優勢:
-
鏈表中的元素在內存中不必是連續的空間,可以充分利用計算機的內存,實現靈活的內存動態管理。
-
鏈表不必在創建時就確定大小,並且大小可以無限地延伸下去。
-
鏈表在插入和刪除數據時,時間復雜度可以達到O(1),相對數組效率高很多。
鏈表的缺點:
- 鏈表訪問任何一個位置的元素時,都需要從頭開始訪問(無法跳過第一個元素訪問任何一個元素)。
- 無法通過下標值直接訪問元素,需要從頭開始一個個訪問,直到找到對應的元素。
- 雖然可以輕松地到達下一個節點,但是回到前一個節點是很難的。
鏈表中的常見操作:
- append(element):向鏈表尾部添加一個新的項;
- insert(position,element):向鏈表的特定位置插入一個新的項;
- get(position):獲取對應位置的元素;
- indexOf(element):返回元素在鏈表中的索引。如果鏈表中沒有該元素就返回-1;
- update(position,element):修改某個位置的元素;
- removeAt(position):從鏈表的特定位置移除一項;
- remove(element):從鏈表中移除一項;
- isEmpty():如果鏈表中不包含任何元素,返回trun,如果鏈表長度大於0則返回false;
- size():返回鏈表包含的元素個數,與數組的length屬性類似;
- toString():由於鏈表項使用了Node類,就需要重寫繼承自JavaScript對象默認的toString方法,讓其只輸出元素的值;
首先需要弄清楚:下文中的position指的是兩個節點之間,並且與index的關系如下圖所示:

position的值一般表示position所指位置的下一個節點。當position的值與index的值相等時,比如position = index = 1,那么它們都表示Node2。
二、封裝單向鏈表類
2.0.創建單向鏈表類
先創建單向鏈表類Linklist,並添加基本屬性,再實現單向鏈表的常用方法:
// 封裝單向鏈表類
function LinkList(){
// 封裝一個內部類:節點類
function Node(data){
this.data = data;
this.next = null;
}
// 屬性
// 屬性head指向鏈表的第一個節點
this.head = null;
this.length = 0;
}
2.1.append(element)
代碼實現:
// 一.實現append方法
LinkList.prototype.append = data => {
//1.創建新節點
let newNode = new Node(data)
//2.添加新節點
//情況1:只有一個節點時候
if(this.length == 0){
this.head = newNode
//情況2:節點數大於1,在鏈表的最后添加新節點
}else {
//讓變量current指向第一個節點
let current = this.head
//當current.next(下一個節點不為空)不為空時,一直循環,直到current指向最后一個節點
while (current.next){
current = current.next
}
// 最后節點的next指向新的節點
current.next = newNode
}
//3.添加完新結點之后length+1
this.length += 1
}
過程詳解:
- 首先讓current指向第一個節點:

- 通過while循環使current指向最后一個節點,最后通過current.next = newNode,讓最后一個節點指向新節點newNode:

測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.測試append方法
list.append('aaa')
list.append('bbb')
list.append('ccc')
console.log(list);
測試結果:

2.2.toString()
代碼實現:
// 實現toString方法
LinkList.prototype.toString = () => {
// 1.定義變量
let current = this.head
let listString = ""
// 2.循環獲取一個個的節點
while(current){
listString += current.data + " "
current = current.next//千萬不要忘了拼接完一個節點數據之后,讓current指向下一個節點
}
return listString
}
測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.插入數據
list.append('aaa')
list.append('bbb')
list.append('ccc')
//3.測試toString方法
console.log(list.toString());
測試結果:

2.3.insert(position,element)
代碼實現:
// 實現insert方法
LinkList.prototype.insert = (position, data) => {
//理解positon的含義:position=0表示新界點插入后要成為第1個節點,position=2表示新界點插入后要成為第3個節點
//1.對position進行越界判斷:要求傳入的position不能是負數且不能超過LinkList的length
if(position < 0 || position > this.length){
return false
}
//2.根據data創建newNode
let newNode = new Node(data)
//3.插入新節點
//情況1:插入位置position=0
if(position == 0){
// 讓新節點指向第一個節點
newNode.next = this.head
// 讓head指向新節點
this.head = newNode
//情況2:插入位置position>0(該情況包含position=length)
} else{
let index = 0
let previous = null
let current = this.head
//步驟1:通過while循環使變量current指向position位置的后一個節點(注意while循環的寫法)
while(index++ < position){
//步驟2:在current指向下一個節點之前,讓previous指向current當前指向的節點
previous = current
current = current.next
}
// 步驟3:通過變量current(此時current已經指向position位置的后一個節點),使newNode指向position位置的后一個節點
newNode.next = current
//步驟4:通過變量previous,使position位置的前一個節點指向newNode
previous.next = newNode
/*
啟示:
1.我們無法直接操作鏈表中的節點,但是可以通過變量指向這些節點,以此間接地操作節點(替身使者);
比如current指向節點3,想要節點3指向節點4只需要:current.next = 4即可。
2.兩個節點間是雙向的,想要節點2的前一個節點為節點1,可以通過:1.next=2,來實現;
*/
}
//4.新節點插入后要length+1
this.length += 1;
return true
}
過程詳解:
inset方法實現的過程:根據插入節點位置的不同可分為多種情況:
- 情況1:position = 0:
通過: newNode.next = this.head,建立連接1;
通過: this.head = newNode,建立連接2;(不能先建立連接2,否則this.head不再指向Node1)

- 情況2:position > 0:
首先定義兩個變量previous和curent分別指向需要插入位置pos = X的前一個節點和后一個節點;
然后,通過:newNode.next = current,改變指向3;
最后,通過:previous.next = newNode,改變指向4;

- 情況2的特殊情形:position = length:
情況2也包含了pos = length的情況,該情況下current和newNode.next都指向null;建立連接3和連接4的方式與情況2相同。

測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.插入數據
list.append('aaa')
list.append('bbb')
list.append('ccc')
//3.測試insert方法
list.insert(0, '在鏈表最前面插入節點');
list.insert(2, '在鏈表中第二個節點后插入節點');
list.insert(5, '在鏈表最后插入節點');
alert(list);
console.log(list);
測試結果:


2.4.get(position)
代碼實現:
//實現get方法
LinkList.prototype.get = (position) => {
//1.越界判斷
// 當position = length時,取到的是null所以0 =< position < length
if(position < 0 || position >= this.length){
return null
}
//2.獲取指定的positon位置的后一個節點的data
//同樣使用一個變量間接操作節點
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
return current.data
}
過程詳解:
get方法的實現過程:以獲取position = 2為例,如下圖所示:
- 首先使current指向第一個節點,此時index=0;

- 通過while循環使current循環指向下一個節點,注意循環終止的條件index++ < position,即當index=position時停止循環,此時循環了1次,current指向第二個節點(Node2),最后通過current.data返回Node2節點的數據;

測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.插入數據
list.append('aaa')
list.append('bbb')
list.append('ccc')
//3.測試get方法
console.log(list.get(0));
console.log(list.get(1));
測試結果:

2.5.indexOf(element)
代碼實現:
//實現indexOf方法
LinkList.prototype.indexOf = data => {
//1.定義變量
let current = this.head
let index = 0
//2.開始查找:只要current不指向null就一直循環
while(current){
if(current.data == data){
return index
}
current = current.next
index += 1
}
//3.遍歷完鏈表沒有找到,返回-1
return -1
}
過程詳解:
indexOf方法的實現過程:
- 使用變量current記錄當前指向的節點,使用變量index記錄當前節點的索引值(注意index = node數-1):

測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.插入數據
list.append('aaa')
list.append('bbb')
list.append('ccc')
//3.測試indexOf方法
console.log(list.indexOf('aaa'));
console.log(list.indexOf('ccc'));
測試結果:

2.6.update(position,element)
代碼實現:
//實現update方法
LinkList.prototype.update = (position, newData) => {
//1.越界判斷
//因為被修改的節點不能為null,所以position不能等於length
if(position < 0 || position >= this.length){
return false
}
//2.查找正確的節點
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
//3.將position位置的后一個節點的data修改成newData
current.data = newData
//返回true表示修改成功
return true
}
測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.插入數據
list.append('aaa')
list.append('bbb')
list.append('ccc')
//3.測試update方法
list.update(0, '修改第一個節點')
list.update(1, '修改第二個節點')
console.log(list);
console.log(list.update(3, '能修改么'));
測試結果:

2.7.removeAt(position)
代碼實現:
//實現removeAt方法
LinkList.prototype.removeAt = position => {
//1.越界判斷
if (position < 0 || position >= this.length) {//position不能為length
return null
}
//2.刪除元素
//情況1:position = 0時(刪除第一個節點)
let current = this.head
if (position ==0 ) {
//情況2:position > 0時
this.head = this.head.next
}else{
let index = 0
let previous = null
while (index++ < position) {
previous = current
current = current.next
}
//循環結束后,current指向position后一個節點,previous指向current前一個節點
//再使前一個節點的next指向current的next即可
previous.next = current.next
}
//3,length-1
this.length -= 1
//返回被刪除節點的data,為此current定義在最上面
return current.data
}
過程詳解:
removeAt方法的實現過程:刪除節點時存在多種情況:
- 情況1:position = 0,即移除第一個節點(Node1)。
通過:this.head = this.head.next,改變指向1即可;
雖然Node1的next仍指向Node2,但是沒有引用指向Node1,則Node1會被垃圾回收器自動回收,所以不用處理Node1指向Node2的引用next。

- 情況2:positon > 0,比如pos = 2即移除第三個節點(Node3)。
注意:position = length時position后一個節點為null不能刪除,因此position != length;
首先,定義兩個變量previous和curent分別指向需要刪除位置pos = x的前一個節點和后一個節點;
然后,通過:previous.next = current.next,改變指向1即可;
隨后,沒有引用指向Node3,Node3就會被自動回收,至此成功刪除Node3 。

測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.插入數據
list.append('aaa')
list.append('bbb')
list.append('ccc')
//3.測試removeAt方法
console.log(list.removeAt(0));
console.log(list.removeAt(0));
console.log(list);
測試結果:

2.8.其他方法
其他方法包括:remove(element)、isEmpty()、size()
代碼實現:
/*-------------其他方法的實現--------------*/
//一.實現remove方法
LinkList.prototype.remove = (data) => {
//1.獲取data在列表中的位置
let position = this.indexOf(data)
//2.根據位置信息,刪除結點
return this.removeAt(position)
}
//二.實現isEmpty方法
LinkList.prototype.isEmpty = () => {
return this.length == 0
}
//三.實現size方法
LinkList.prototype.size = () => {
return this.length
}
測試代碼:
//測試代碼
//1.創建LinkList
let list = new LinkList()
//2.插入數據
list.append('aaa')
list.append('bbb')
list.append('ccc')
/*---------------其他方法測試----------------*/
//remove方法
console.log(list.remove('aaa'));
console.log(list);
//isEmpty方法
console.log(list.isEmpty());
//size方法
console.log(list.size());
測試結果:

2.9.完整實現
// 封裝鏈表類
function LinkList(){
// 封裝一個內部類:節點類
function Node(data){
this.data = data;
this.next = null;
}
// 屬性
// 屬性head指向鏈表的第一個節點
this.head = null;
this.length = 0;
// 一.實現append方法
LinkList.prototype.append = data => {
//1.創建新節點
let newNode = new Node(data)
//2.添加新節點
//情況1:只有一個節點時候
if(this.length == 0){
this.head = newNode
//情況2:節點數大於1,在鏈表的最后添加新節點
}else {
//讓變量current指向第一個節點
let current = this.head
//當current.next(下一個節點不為空)不為空時,一直循環,直到current指向最后一個節點
while (current.next){
current = current.next
}
// 最后節點的next指向新的節點
current.next = newNode
}
//3.添加完新結點之后length+1
this.length += 1
}
// 二.實現toString方法
LinkList.prototype.toString = () => {
// 1.定義變量
let current = this.head
let listString = ""
// 2.循環獲取一個個的節點
while(current){
listString += current.data + " "
current = current.next//千萬不要忘了拼接完一個節點數據之后,讓current指向下一個節點
}
return listString
}
// 三.實現insert方法
LinkList.prototype.insert = (position, data) => {
//理解positon的含義:position=0表示新界點插入后要成為第1個節點,position=2表示新界點插入后要成為第3個節點
//1.對position進行越界判斷:要求傳入的position不能是負數且不能超過LinkList的length
if(position < 0 || position > this.length){
return false
}
//2.根據data創建newNode
let newNode = new Node(data)
//3.插入新節點
//情況1:插入位置position=0
if(position == 0){
// 讓新節點指向第一個節點
newNode.next = this.head
// 讓head指向新節點
this.head = newNode
//情況2:插入位置position>0(該情況包含position=length)
} else{
let index = 0
let previous = null
let current = this.head
//步驟1:通過while循環使變量current指向position位置的后一個節點(注意while循環的寫法)
while(index++ < position){
//步驟2:在current指向下一個節點之前,讓previous指向current當前指向的節點
previous = current
current = current.next
}
// 步驟3:通過變量current(此時current已經指向position位置的后一個節點),使newNode指向position位置的后一個節點
newNode.next = current
//步驟4:通過變量previous,使position位置的前一個節點指向newNode
previous.next = newNode
//我們無法直接操作鏈表中的節點,但是可以通過變量指向這些節點,以此間接地操作節點;
}
//4.新節點插入后要length+1
this.length += 1;
return true
}
//四.實現get方法
LinkList.prototype.get = (position) => {
//1.越界判斷
// 當position = length時,取到的是null所以0 =< position < length
if(position < 0 || position >= this.length){
return null
}
//2.獲取指定的positon位置的后一個節點的data
//同樣使用一個變量間接操作節點
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
return current.data
}
//五.實現indexOf方法
LinkList.prototype.indexOf = data => {
//1.定義變量
let current = this.head
let index = 0
//2.開始查找:只要current不指向null就一直循環
while(current){
if(current.data == data){
return index
}
current = current.next
index += 1
}
//3.遍歷完鏈表沒有找到,返回-1
return -1
}
//六.實現update方法
LinkList.prototype.update = (position, newData) => {
//1.越界判斷
//因為被修改的節點不能為null,所以position不能等於length
if(position < 0 || position >= this.length){
return false
}
//2.查找正確的節點
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
//3.將position位置的后一個節點的data修改成newData
current.data = newData
//返回true表示修改成功
return true
}
//七.實現removeAt方法
LinkList.prototype.removeAt = position => {
//1.越界判斷
if (position < 0 || position >= this.length) {
return null
}
//2.刪除元素
//情況1:position = 0時(刪除第一個節點)
let current = this.head
if (position ==0 ) {
//情況2:position > 0時
this.head = this.head.next
}else{
let index = 0
let previous = null
while (index++ < position) {
previous = current
current = current.next
}
//循環結束后,current指向position后一個節點,previous指向current前一個節點
//再使前一個節點的next指向current的next即可
previous.next = current.next
}
//3,length-1
this.length -= 1
//返回被刪除節點的data,為此current定義在最上面
return current.data
}
/*-------------其他方法的實現--------------*/
//八.實現remove方法
LinkList.prototype.remove = (data) => {
//1.獲取data在列表中的位置
let position = this.indexOf(data)
//2.根據位置信息,刪除結點
return this.removeAt(position)
}
//九.實現isEmpty方法
LinkList.prototype.isEmpty = () => {
return this.length == 0
}
//十.實現size方法
LinkList.prototype.size = () => {
return this.length
}
}
參考資料:JavaScript數據結構與算法
