在前端開發中,常常接觸到的線性結構有數組
、字符串
,還有 ES6 中的Set
和Map
。這其中最經常使用的,應該就數數組了。咱們今天要說到的鏈表也是數據元素的線性集合,跟數組和字符串不一樣的是,鏈表元素的線性順序不是由他們在內存中的物理位置給出的,而是由他一個又一個的節點的指向串起來的序列表現出來的。這樣設計的目的是爲了解決數組等數據結構須要預先知道數據大小的缺點,開發 js 的同窗可能不太瞭解,由於 js 中的數組是支持動態擴容的,(感興趣的朋友移步這片好文『v8 引擎下的「數組」底層實現』)前端
鏈表在插入的時候很快,能夠達到O(1)
的複雜度,但他的訪問時間是線性的,更快的訪問,如隨機訪問,是不可行的。與鏈表相比,數組具備更好的緩存位置。 今天就來講說這個鏈表,以及它在 js 中的實現。node
鏈表是一種鏈式存取的數據結構,用一組地址任意的存儲單元存放線性表中的數據元素。 鏈表中的基本數據以節點來表示,每一個節點由元素+指針構成,元素是存儲數據的存儲單元,指針就是鏈接每一個節點的地址數據。 來源百度數組
鏈表中又分爲單鏈表、雙鏈表和循環鏈表等,其中最簡單的要數單鏈表了緩存
單鏈表的的節點只有兩個域,一個是信息域,一個是指針域。信息域會保存當前節點的數據信息,而指針域保存着指向下個節點的地址,最末尾的節點的指針域指向null
。 介紹到這裏的時候,鏈表這個名字的由來就一清二楚了。每一個節點保存數據的同時,還指向下個節點,而後下個節點繼續保存數據的同時指向它的下個節點,一個個節點就這麼連接起來,造成了一個鏈條。markdown
單鏈表由最開始的head
一直指向null
。中的鏈條節點,data
存放數據,next
存放下個節點的地址數據。 因此單個的節點,實現起來就很簡單了數據結構
class Node{
constructor(element){
// 信息域
this.element = element;
// 指針域
this.next = null;
}
}
複製代碼
同時這個鏈表數據還要實現增刪改查的功能。app
class SignleLinkLit{
constructor(){
// 初始的頭部節點
this.head = null
// 初始長度
this.length = 0;
}
// 獲取鏈表
getList() {
return this.head;
}
// 鏈表長度
size() {
return this.length;
}
// 鏈表是否爲空
isEmpty() {
return this.length === 0;
}
// 追加
append(element) {}
// 搜尋
search(list, element) {}
// 插入
insert(position, element) {}
// 移除
remove(element) {}
}
複製代碼
getList
、size
和isEmpty
這三個方法自沒必要說了,咱們着重說說另外四個操做鏈表數據的方法。oop
追加節點post
因爲鏈表的的特性,追加節點只需遍歷到尾部節點,並將其指針域指向待追加的節點就能夠了this
// 追加節點
append(element){
// 先定義當前節點
const node = new Node(element);
// 用來輔助找到鏈尾的變量
let temp = this.head;
if(!this.head){ // 若是當前head爲空,就直接放置element
this.head = node;
}else{
// INFO: 當節點的next爲null時,便可肯定找到鏈尾
while(temp.next){ // 循環遍歷以找到鏈尾
temp = temp.next;
}
temp.next = node;
}
this.length++;
}
複製代碼
搜尋節點
從頭至尾遍歷單鏈條,判斷節點是否等於查找的值,相等則返回true
,不想等就返回false
。
search(element){
if(!this.head) return false;
let temp = this.head;
while(temp){
if(temp.element === element) return true;
temp = temp.next
}
return false;
}
複製代碼
插入節點
步數(position)爲 0 的時候,直接將節點的next
指向head
,再將節點賦值給this.head
。position 不爲 0,就遍歷到 position 前一個節點插入。
insert(position, element){
if(position < 0 || position > this.length) return null;
const node = new Node(element);
if(position === 0){
node.next = this.head;
this.head = node;
}else{
let temp = this.head,
index = 0;
while(index < position){
temp = temp.next;
index++;
}
// 插入操做,將待插入節點的next指向當前節點的next
node.next = temp.next;
// 而後將當前節點的next指向待插入的節點
temp.next = node;
}
// 長度加一
this.length++;
}
複製代碼
刪除節點
一樣是遍歷單鏈表,找到待刪的節點,將其刪之。 須要注意的是,當待刪除的節點是head
時,須要單獨「重定向」head
的指向。
remove(element){
if(!this.head) return;
if(this.head.element === element){
this.head = this.head.next;
this.length--;
return;
}
let curr = this.head,
prev = this.head;
while(curr){
if(curr.element !== element){
prev = curr;
curr = curr.next;
}else{
prev.next = curr.next;
this.length--;
break;
}
}
}
複製代碼
前面的單鏈表只有一個從頭鏈到尾的方向,而這個雙鏈表則是有兩個方向,支持從尾鏈到頭
因此這裏雙鏈表裏面的單個節點元素跟上面的單鏈表節點元素就有所不一樣了:
class Node {
constructor(element) {
this.element = element;
// 前驅指針
this.prev = null;
// 後繼指針
this.next = null;
}
}
複製代碼
雙鏈表的大概樣子:
class DoublyLinkedList{
constructor(){
// 初始頭部節點
this.head = null;
// 初始尾部節點
this.tail = null;
// 鏈表的長度
this.length = 0;
}
// 操做
size(){
return this.length;
}
// 獲取鏈表
getList(isInverted = false){
return isInverted ? this.tail : this.head;
}
// 清空鏈表
clear(){
this.head = this.tail = null;
this.length = 0;
}
// 鏈表是否爲空
isEmpty(){
return this.length === 0;
}
// 插入節點
insert(position, element);
// 刪除鏈表節點
removeAt(position){}
// 尋找鏈表節點
search(element){}
}
複製代碼
插入節點
這裏須要畫個圖來輔助下理解了。首先初始化一個待插入的節點,遍歷到鏈表的position
的前一個位置節點,在該節點位置插入待插入的節點,處理好周圍三個節點的先後指針。
insert(position, element){
if(position < 0 || position > this.length) return null;
const node = new Node(element);
if(!this.head){
this.head = this.tail = node;
}else if(position === 0){ // 插入節點是0的話,就須要調整head指向
node.next = this.head;
this.head.prev = node;
// head指向新的頭節點
this.head = node;
}else if(position === this.length){ // 是尾部
this.tail.next = node;
node.prev = this.tail;
// tail重定向
this.tail = node;
}else {
let temp = this.head,
index = 0;
while(index < position){
temp = temp.next;
index++;
}
temp.prev.next = node;
node.prev = temp.prev;
temp.prev = node;
node.next = temp;
}
this.length++;
}
複製代碼
刪除節點
這裏跟單鏈表相似,先遍歷鏈表,找到須要刪除的節點後,將周圍的節點的prev
和next
重定向。
removeAt(position){
if(!this.length || position < 0 || position > this.length - 1) return null;
let temp = this.head, index = 0;
if(this.length === 1){ // 若是僅有一個節點
this.clear();
}else if(position === 0){
this.head.next.prev = null;
this.head = this.head.next;
}else if(position === this.length - 1){
this.tail.prev.next = null;
this.tail = this.tail.prev;
}else{
while(index < position){
temp = temp.next;
index ++;
}
temp.prev.next = temp.next;
temp.next.prev = temp.prev;
}
this.length--;
return temp.element;
}
複製代碼
搜索節點
跟單鏈表相似,從頭到尾遍歷鏈表,找到元素返回true
,不然返回false
。
search(element){
let temp = this.head;
while(temp){
if(temp.element === element) return true;
temp = temp.next;
}
return false;
}
複製代碼
將兩個升序鏈表合併爲一個新的升序鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。
示例:
輸入:1->2->4, 1->3->4 輸出:1->1->2->3->4->4
// Definition for singly-linked list.
// 節點
class ListNode {
constructor(val){
this.val = val;
this.next = null;
}
}
複製代碼
我最開始的方法
var mergeTwoLists = function(l1, l2) {
let res = new List();
while (l1 !== null && l2 !== null) {
if (l1.element < l2.element) {
res.append(l1.element);
l1 = l1.next;
} else {
res.append(l2.element);
l2 = l2.next;
}
}
let temp = !l1 ? l2 : l1;
while (temp) {
res.append(temp.element);
temp = temp.next;
}
return res;
};
複製代碼
還有更優雅的使用遞歸的方式
var mergeTwoLists = function(l1, l2) {
if (l1 === null) {
return l2;
}
if (l2 === null) {
return l1;
}
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l2.next, l1);
return l2;
}
}
複製代碼
關注本人公衆號
本文使用 mdnice 排版