链表
本文描述了链表的基本概念和常用操作,并给出了几道常见的链表算法题来帮助读者理解链表。
基本概念
链表是数据元素的线性组合,但和数组不同的是,链表中的数据在内存中不是连续存储的。在一个单向链表中,每个节点指向后一个节点,这些节点前后相连并形成了一个序列。而在非循环双向链表中,除第一个和最后一个节点外,一个节点通常会和它的前后两个节点相连。此外,在实际使用双向链表时,通常会设置一个前哨节点作为表头,以方便对双向链表进行操作。
常用操作
单链表
操作代码
// 构建单链表
function ListNode(val){
this.val = val;
this.next = null;
}
// 向链表末尾添加节点
function add(head, val){
let node = new ListNode(val);
if(head == null){
head = node;
}else{
let cur = head;
while(cur.next != null){
cur = cur.next;
}
cur.next = node;
}
}
// 向链表表头前插节点
function prepend(head, val){
let node = new ListNode(val);
node.next = head;
head = node;
}
// 按值搜索链表节点
function contains(head, val){
let cur = head;
while(cur != null){
if(cur.val == val){
return true;
}
cur = cur.next;
}
return false;
}
// 删除节点
function remove(head, val){
if(head == null){
return false;
}
let cur = head.next, front = head;
while(cur != null && cur.val != val){
front = cur;
cur = cur.next;
}
if(cur != null){
front.next = cur.next;
cur = null;
return true;
}else{
return false;
}
}
// 遍历链表
function traverse(head){
let cur = head;
while(cur != null){
console.log(cur.val);
cur = cur.next;
}
}
// 反向遍历
function reverseTraverse(head){
let tail = null, cur = head;
while(cur != null){
let node = new ListNode(cur.val);
node.next = tail;
tail = node;
cur = cur.next;
}
traverse(tail);
}
// 创建表头
let head = new ListNode(0);
复杂度
- 时间复杂度:
- 空间复杂度:O(n)
双向链表
操作代码
// 定义双链表
function ListNode(value){
this.value = value;
this.previous = null;
this.next = null;
}
// 插入节点到前哨节点后
function addNode(head, value){
let node = new ListNode(value);
if(head.next == null){
head.previous = node;
}
node.previous = head;
node.next = head.next;
head.next = node;
}
// 删除指定值节点
function deleteNode(head, value){
let cur = head.next;
while(cur != null){
if(cur.value = value){
cur.previous.next = cur.next;
cur.next.previous = cur.previous;
}
cur = cur.next;
}
}
// 反向遍历
function reverseTraverse(head){
let cur = head.previous;
while(cur != null){
console.log(cur.value);
cur = cur.previous;
}
}
//创建双链表对象 并将其作为前哨节点
let head = new ListNode("");
复杂度
- 时间复杂度:
- 空间复杂度:O(n)
实战练习
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {number[]}
*/
var reversePrint = function(head) {
let arr = [];
let cur=head;
while(cur != null){
arr.push(cur.val);
cur=cur.next;
}
return arr.reverse();
};
/**题目分析
* 反向打印链表,而题目给出的单向链表又只能从头访问,这和栈的思想类似,
* 所以可以利用js数组模拟入栈出栈来解题。
*/
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var deleteNode = function(head, val) {
if(head.val == val){
return head.next;
}
let node = head.next, front = head;
while(node != null){
if(node.val == val){
front.next = node.next;
}
front = node;
node = node.next;
}
return head;
};
/**题目分析
* 很简单的一道题,就是单向链表的节点删除操作。
*/
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var getKthFromEnd = function(head, k) {
let fast = slow = head
while(fast != null){
fast = fast.next;
if(k-- <= 0){
slow = slow.next;
}
}
return slow;
};
/**题解
* 像这种找第几个节点的,基本都是快慢指针方法,
* 即第一个节点负责遍历,走到末尾,第二个节点延迟行动,使得当遍历结束时,第二个节点正好指向目标节点。
* 这里需要注意,遍历结束时,fast指针为null,而不是最后一个节点,所以应该是用后自减,使得快慢指针至少差一个节点位置。
*/
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let reverseHead = null, cur = head;
while(cur != null){
const next = cur.next;
cur.next = reverseHead;
reverseHead = cur;
cur = next;
}
return reverseHead;
};
/**题解
* 反转链表,其实就是使用前插方法来重新构建一个新链表。
*/
/**
* // Definition for a Node.
* function Node(val, next, random) {
* this.val = val;
* this.next = next;
* this.random = random;
* };
*/
/**
* @param {Node} head
* @return {Node}
*/
var copyRandomList = function(head) {
if(head == null){
return null;
}
let copyArr = [], cur = head, index = 0;
while(cur != null){
let node = new Node(cur.val, null, null);
copyArr.push(node);
cur.val = index++;
cur = cur.next;
}
let copyHead = copyArr[0];
let copyCur = copyHead;
cur = head;
while(cur != null){
copyCur.next = cur.next? copyArr[cur.next.val] : null;
copyCur.random = cur.random? copyArr[cur.random.val] : null;
cur = cur.next;
copyCur = copyCur.next;
}
return copyHead;
};
/**题解
* 一种思路是:先遍历链表,并使用映射表,将链表中的节点作为key,将对应的复制节点(复制值和next指针)作为值存储。
* 之后再做一个遍历,使用哈希表获取random节点的复制节点,更新random指针。
*
* 另一种思路也是两次循环,但不需要映射表,用数组即可。在第一次循环时修改value值为其数组索引,之后根据value来更新next和random。
*/
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
let lenA = 0, lenB = 0;
let curA = headA, curB = headB;
while(curA != null){
lenA++;
curA = curA.next;
}
while(curB != null){
lenB++;
curB = curB.next;
}
curA = headA, curB = headB;
if(lenA < lenB){
[lenA, lenB] = [lenB, lenA];
[curA, curB] = [curB, curA];
}
let gap = lenA - lenB;
while(gap > 0){
curA = curA.next;
gap--;
}
while(curA!=null){
if(curA == curB){
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
};
/**题解
* 这道题也是快慢指针的思路。当两个链表存在公共节点时,公共节点及后面的节点长度一致。
* 所以先遍历得到两个链表的长度差,然后使用快慢指针遍历即可。
*/
总结
这几道比较简单,基本上都是一些链表的基本操作。一个值得注意的点是快慢指针(双指针)的使用,使用快慢指针能帮助减少时间\空间复杂度。
参考
[1] github-javascript-algorithms,单向链表的基本概念和基本操作
[2] github-javascript-algorithms,双向链表的基本概念和基本操作