链表是一种有序的数据结构,不同于数组,链表从起点或者中间插入或移动项的成本很小。因为链表是使用指针指向下一个元素。每个元素存储元素的本身节点和指向下一个元素的引用。相对于传统的数组,链表需要使用指针,在数组中,我们可以直接访问位置的任何元素,而想要访问链表中间的一个元素,则需要从(表头)开始迭代链表,知道找到所需的元素。
创建链表类
由于js没有天生提供链表的类,这里我们用数组快速生成链表。生成链表,我们提供一个基础的createNode方法,用于生成链表节点。利用count存储链表的长度,利用head存放链表(表头)。
class NodeList {
constructor(arr) {
if (arr.length) {
this.head = this.createNode(arr[0]);
let p = this.head;
this.count = arr.length;
for (let i = 1; i < arr.length; i++) {
p.next = this.createNode(arr[i]);
p = p.next;
}
} else {
this.head = null;
this.count = 0;
}
}
createNode(element, next) {
return next ? { val: element, next } : { val: element, next: null };
}
}
遍历链表
不同于数组,我们遍历数组有现成的方法,例如map、forEach,对于链表,我们需要使用指针遍历。
对于以下链表:
const {head} = new NodeList(['a','b','c','d','e']);
遍历操作如下:
let p = head;
while(p) {
console.log(p.val);
p = p.next;
}
移除链表中的元素
对于移除链表中的元素,我们提供一个参数index。参考数组中移除行为,我们将移除的链表元素返回出去。
- 移除链表分为两种情况,一种情况是移除表头第一个元素。 这样我们可以直接让head赋值上head的next。这样就直接删除了表头的第一个元素。
- 移除链表的第二种情况,就是删除中间或者后续的元素。这块我们也是一样的思路,利用一个指针prev存储curr的上一个元素。 然后用将上一个元素的next指向curr的next。这样,curr元素就从链表当中断开了。
class NodeList {
removeNode(index) {
if (index >= 0 && index < this.count) {
let curr = this.head;
if (index === 0) {
this.head = curr.next;
} else {
let prev;
for (let i = 0; i < index; i++) {
prev = curr;
curr = curr.next;
}
prev.next = curr.next;
}
this.count -= 1;
return curr.val;
} else {
return undefined;
}
}
}
链表元素的删除,基本上是需要获得上一个链表元素,然后将上一个链表元素指向下一个链表元素的next的。
算法题巩固
删除链表中的节点leetCode 237
链接:leetcode-cn.com/problems/de…
代码思路:
因为是非末尾节点, 且直接给定我们一个节点。 要我们删除元素。 这里边,因为我们删除节点通常都是需要节点的上一个节点的。 但是,这里只给出了当前节点,这里我们就没办法获取上一个节点了。 我们可以换一个思路。 就是将当前节点变成下一个节点, 然后再删除下一个节点。达到目的。
- 将当前节点变成下一个节点的值。
- 删除下一个节点。
/**
* @param {ListNode} node
* @return {void} Do not return anything, modify node in-place instead.
*/
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
};
反转链表leetCode206
链接:leetcode-cn.com/problems/re…
代码思路:
反转链表的思路很简单,我们只需要用双指针,不断的将curr.next指向prev就行。而且,因为我们反转链表的开始节点其实是原来的结束节点。所以,我们需要将prev指针先设置成null。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let prev = null
let curr = head;
while(curr) {
const next = curr.next;
curr.next = prev; // 因为此处需要更改curr,但是prev又要指向原有的curr去移动指针,所以,我们需要先将curr去存储起来。
prev = curr;
curr = next;
}
return prev;
};
两数相加
链接:leetcode-cn.com/problems/re…
代码思路:
因为得到的是两个倒序的链表,返回值也是倒序的链表,所以,我们刚好可以像基础数学的相加一样进行链表相加操作,对每一位进行相加,如果有进位就将其存储到carry当中。下一位相加时,加上carry即可。
- 用两个指针遍历链表。
- 只要有一个链表没有遍历完成,那么就得继续进行相加操作。如果链表一长一短,则短的那个链表取的节点值为0进行相加操作。并加上carry
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
let p1 = l1;
let p2 = l2;
let resultList = new ListNode(0);
let p3 = resultList;
while(p1 || p2 || carry){
// 因为当两个链表都加完的时候,如果有进位,我们还需要加上一位,所以这里将carry也加入到while条件中
const num1 = p1 && p1.val || 0;
const num2 = p2 && p2.val || 0;
let carry = 0;
const res = (num1 + num2 + carry) % 10; // 得到当前位的值
carry = Math.floor((num1 + num2 + carry) / 10); // 得到进位的值
p3.next = new ListNode(res); // 生成赋值节点
p3 = p3.next; // 控制结果链表的指针指向下一个节点
if (p1) { p1 = p1.next} // 控制链表节点走向下一个节点
if (p2) { p2 = p2.next}
}
return resultList.next;
};
删除排序链表中的重复元素 leetCode 83
链接: leetcode-cn.com/problems/re…
代码思路:
因为是排序链表,所以如果元素重复了。那么下一个元素肯定等于当前节点的元素。所以,是否删除下一个节点的依据就是下一个元素是否相等。然后我们可以控制指针继续往下移动。
注意点:
如果多个重复节点的出现,可能会漏删元素。 例如[1,1,1]在这种情况下,我们的指针就先不要移动,继续删除一个节点。再进行移动。
代码实现:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
let p = head;
while(p && p.next) {
if (p.val === p.next.val) {
p.next = p.next.next;
} else {
p = p.next;
}
}
return head;
};
环形链表 leetCode 141
链接: leetcode-cn.com/problems/li…
代码思路:
这里我们可以从生活中的一些现象中去进行考虑。 例如,如果两个赛车存在速度差。那么在一个环形跑道中就一定会重新相遇。 即超过一圈。 那么,只要链表中两个指针能够重合,那么就证明是"环形跑道"。如果链表能遍历完成,则一定不是环形跑道。
- 定义一个慢指针,每次步进1个元素。
- 定义一个快指针,每次步进2个元素。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
let p1 = head;
let p2 = head;
while(p1 && p2 && p2.next) {
// 因为我们需要一个快指针,快指针是步进2个元素的,为了防止链表不够长度报错,所以必须存在p2.next
p1 = p1.next;
p2 = p2.next.next;
if (p1 === p2) {
return true;
}
}
return false; // 链表能正常遍历完成,则肯定不是环形链表
};