链表理论基础
单链表
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向 null(空指针)。
链表的入口节点称为链表的头结点也就是 head。
双链表
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表既可以向前查询也可以向后查询。
循环链表
循环链表:就是链表收尾相连。
循环链表可以用来解决约瑟夫环问题。
链表的存储方式
链表是通过指针域的指针链接在内存中各个节点,所以,链表中的节点不是连续分布的,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
链表的定义
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
删除节点
如图所示:只需要将 C 节点的指针域指向 E 节点即可。
添加节点
如图所示:只需要将 C 节点的指针域指向 F 节点,F 节点的指针域指向 D 节点即可。
性能分析
查询(时间复杂度) | 插入/删除(时间复杂度) | 使用场景 | |
---|---|---|---|
数组 | O(1) | O(n) | 数据量固定,频繁查询,较少增删 |
链表 | O(n) | O(1) | 数据量不固定,频繁增删,较少查询 |
移除链表元素
/*
思路:创建虚拟头结点,从头开始与链表元素一一比对,直到找到对应节点
*/
var removeElements = function(head, val) {
// 创建一个虚拟头节点
const ret = new ListNode(0, head);
let cur = ret;
while(cur.next) {
// 找到指定节点
if(cur.next.val === val) {
// 让指针指向下一个节点
cur.next = cur.next.next;
continue;
}
// 如果不是当前节点,则继续找下一个节点
cur = cur.next;
}
return ret.next;
};
设计链表
/*
链表节点
*/
function LinkNode(val, next) {
this.val = (val === undefined ? 0 : val); // 节点值
this.next = (next === undefined ? null : next); // 节点的下一个节点
}
/*
创建空链表
*/
var MyLinkedList = function() {
this._size = 0; // 链表长度
this._tail = null; // 头结点
this._head = null; // 尾结点
};
/*
获取目标结点
*/
MyLinkedList.prototype.getNode = function(index) {
// 如果索引越界
if(index < 0 || index >= this._size) return null;
// 创建虚拟头节点
let cur = new LinkNode(0, this._head);
// 从头开始迭代,直到 index 减为0(找到了)
while(index-- >= 0) {
cur = cur.next;
}
return cur;
}
/*
获取目标结点的值
*/
MyLinkedList.prototype.get = function(index) {
// 如果索引越界
if(index < 0 || index >= this._size) return -1;
// 获取当前结点
return this.getNode(index).val;
};
/*
添加头结点
*/
MyLinkedList.prototype.addAtHead = function(val) {
// 创建头结点
const node = new LinkNode(val, this._head);
// 把头结点设置为创建节点
this._head = node;
// 链表的长度+1
this._size++;
// 如果原链表是空链表
if(!this._tail) {
this._tail = node;
}
};
/*
添加尾结点
*/
MyLinkedList.prototype.addAtTail = function(val) {
// 创建尾结点
const node = new LinkNode(val, null);
// 链表的长度+1
this._size++;
// 如果原链表不为空
if(this._tail) {
// 将原链表的尾结点的 next 指针指向 node
this._tail.next = node;
// 设置新的尾结点
this._tail = node;
return;
}
// 如果原链表为空
this._tail = node;
this._head = node;
};
/*
添加目标结点
*/
MyLinkedList.prototype.addAtIndex = function(index, val) {
// 索引越界
if(index > this._size) return null;
// 目标结点为头结点
if(index <= 0) {
this.addAtHead(val);
return;
}
// 目标结点为尾结点的下一个结点(即添加一个尾结点)
if(index == this._size) {
this.addAtTail(val);
return;
}
// 获取目标结点的上一个结点
const node = this.getNode(index - 1);
// 将 node 结点的 next 指针指向新结点,新结点的 next 指针指向目标结点
node.next = new LinkNode(val, node.next);
this._size++;
};
/*
删除目标结点
*/
MyLinkedList.prototype.deleteAtIndex = function(index) {
// 索引越界
if(index < 0 || index >= this._size) return;
// 如果是目标结点是头结点
if(index == 0) {
// 设置头结点指向头结点的下一个结点
this._head = this._head.next;
// 如果这个结点同时也是尾结点(即:链表只有一个结点)
if(index == this._size - 1) {
this._tail = this._head;
}
this._size--;
return;
}
// 获取目标结点的下一个结点
const node = this.getNode(index - 1);
// 设置 node 结点的 next 指针指向目标结点的下一个结点
node.next = node.next.next;
// 如果目标结点是尾结点
if(index == this._size - 1) {
this._tail = node;
}
this._size--;
};
反转链表
/*
双指针
思路:从头节点开始遍历,修改节点的指针指向前一个节点
*/
var reverseList = function(head) {
// 如果是空链表或链表只有一个节点
if(!head || !head.next) return head;
let pre = null, cur = head;
while(cur) {
[cur.next, pre, cur] = [pre, cur, cur.next];
}
return pre;
};
/*
递归
思路:从头结点开始递归,修改节点的指针指向前一个节点
*/
var reverseList = function(head) {
var reverse = function(pre, head) {
// 如果链表为空链表
if(!head) return pre;
// temp:暂存节点
const temp = head.next;
// 修改节点的指针指向前一个节点
head.next = pre;
// 左指针改为当前结点
pre = head
return reverse(pre, temp);
}
return reverse(null, head);
};
/*
递归
思路:从尾节点开始递归,修改节点的指针指向前一个结点
*/
var reverseList = function(head) {
var reverse = function(head) {
// 节点为空 或 节点为链表最后一个结点
if(!head || !head.next) return head;
// 从后往前翻
// next:head的下一个结点
const next = reverse(head.next);
[head.next, next.next] = [next.next, head];
return head;
}
let cur = head;
// 找到尾节点的前一个结点
while(cur && cur.next) {
cur = cur.next;
}
reverse(head);
return cur;
};
两两交换链表中的节点
/*
思路:创建虚拟头节点,从头节点开始遍历,依次两两交换节点
*/
var swapPairs = function(head) {
// ret:虚拟头节点
// temp:临时节点
let ret = new ListNode(0, head), temp = ret;
// 当 临时结点的下一个节点 和 下下一个节点 都存在时
while(temp.next && temp.next.next) {
// cur:要交换的右节点
// pre:要交换的左节点
let cur = temp.next.next, pre = temp.next;
// 交换节点
[pre.next, cur.next, temp.next] = [cur.next, pre, cur];
// 继续交换后续节点
temp = pre;
}
return ret.next;
};
删除链表的倒数第N个节点
/*
双指针
思路:创建虚拟头结点,要找到倒数第n个节点,只需要找到正数第n个节点(右指针),然后从头结点(左指针)开始依次向右移动,当右指针移动到尾节点的下一个节点(null)时,此时的左指针就是倒数第n个节点
*/
var removeNthFromEnd = function(head, n) {
// node:虚拟头节点
let node = new ListNode(0, head);
// pre:左指针
// cur:右指针
let pre = node, cur = head;
// 找到第n个节点(cur)
while(n > 0) {
cur = cur.next;
n--;
}
// 找到要删除的节点(pre)
while(cur != null) {
pre = pre.next;
cur = cur.next;
}
// 删除节点
pre.next = pre.next.next;
return node.next;
};
链表相交
/*
思路:因为链表相交部分的长度是相同的,所以先求出链表的长度差,找到更长的链表的起始比较节点,将该节点与另一链表头节点进行比较,如果不同则继续比较后续节点,直到找到相同的节点
*/
var getIntersectionNode = function(headA, headB) {
/*
获取链表长度
head:头结点
*/
var getLength = function(head) {
// 定义长度变量
let len = 0;
// 创建判断长度的节点
let cur = head;
// 求出链表的长度
while(cur) {
cur = cur.next;
len++;
}
return len;
}
// lenA:链表A的长度
// lenB:链表B的长度
let lenA = getLength(headA), lenB = getLength(headB);
// curA:链表A中的相交节点
// curB:链表B中的相交节点
let curA = headA, curB = headB;
// 链表B更长
while(lenA < lenB) {
curB = curB.next;
lenB--;
}
// 链表A更长
while(lenA > lenB) {
curA = curA.next;
lenA--;
}
// 遍历 curA 和 curB,若相同则返回
while(curA && curA !== curB) {
curA = curA.next;
curB = curB.next;
}
return curA;
};
环形链表
/*
思路:如下图所示,慢指针的步数为(x + y),快指针的步数为(x + n(y + z) + y),则有 2(x + y) = x + n(y + z) + y => x = (n - 1)(y + z) + z,即快指针从相遇处出发,慢指针从头节点出发,它们一定会在环的入口处相遇
*/
var detectCycle = function(head) {
// 空链表 或 单节点链表
if(!head || !head.next) return null;
// fast:快指针
// slow:慢指针
let fast = head.next.next, slow = head.next;
// 当快、慢指针没有相遇 且 快指针不为null 且 快指针的下一个节点不为null
while(fast !== slow && fast && fast.next) {
// 快指针每次走两个节点
fast = fast.next.next;
// 慢指针每次走一个节点
slow = slow.next;
}
// 不存在环
if(!fast || !fast.next) return null;
// 快、慢指针相遇了,现在要找到环的入口
slow = head;
// 遍历快、慢指针,直到快、慢指针再次相遇的节点即为入口节点
while(slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
};
注意:x = (n - 1)(y + z) + z,即只要将慢指针从头结点开始、快指针从相遇节点开始遍历,一定会在入口节点再次相遇