文章目录
链表
链表存储有序的元素集合,但链表中的元素在内存中并不是连续的,每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针)组成,如下图所示:
相对于数组,链表的好处在于,添加或移除元素时不需要移动其他元素。但是若想要访问链表中的元素,就必须从链表起点开始查找,而不能像数组通过索引下标来查找元素。
//创建一个链表,并为其添加一些方法——来自于《学习JavaScript数据结构与算法》
function LinkedList(){
//定义一个节点
var Node=function(element){
this.element=element;
this.next=null;
};
var length=0;
var head=null;
//向链表尾部追加元素,链表尾部元素的下一个元素始终为null
this.append=function(element){
//node元素被创建时,它的next指针始终为null
var node=new Node(element);
var current;
//当head为null,说明是一个空链表
if(head===null){
head=node;
}else{
//链表不为空,因此需要找到尾部元素,尾部元素的下一个结点为null,因此需要找到下一个结点为null的元素
current=head;
//循环列表,直到找到最后一项
while(current.next){
current=current.next;
}
//找到最后一项,将其next赋值为node,建立链接
current.next=node;
}
length++; //更新链表长度
};
//向链表中的任意位置插入一个新的项
this.insert=function(position,element){
if(position >= 0 && position <= length){
var node = new Node(element);
var current = head, previous;
var index = 0;
if(position === 0){ //z在第一个位置添加
node.next = current;
head = node;
}else{
while(index++ < position){
previous = current;
current = current.next;
}
node.next = current;
previous.next = node;
}
}else{
return false;
}
};
//从链表的特定位置移除一项
this.removeAt=function(position){
if(position>-1 && position<length){
var current=head;
var previous;
var index=0;
//移除第一项
if(position===0){
head=current.next; //current.next=null
}else{
while(index++ <position){
previous=current;
current=current.next;
}
//将previous与current的下一项连接起来,跳过current,从而移除它
previous.next=current.next;
}
length--;
return current.element;
}else{
return null;
}
};
//从链表中移除一项
this.remove=function(element){
var index = this.indexOf(element);
return this.removeAt(index);
};
//返回元素在链表中的索引,不存在则返回-1
this.indexOf=function(element){
var current = head;
var index = -1;
while(current){
if(element === current.element){
return index;
}
index++;
current = current.next;
}
return -1;
};
//判断链表是否为空,为空返回true,有值返回false
this.isEmpty=function(){
return length === 0;
};
//返回链表包含的元素个数,与length属性类似
this.size=function(){
return length;
};
this.getHead = function(){
return head;
}
//将LinkedList对象转换成一个字符串
this.toString=function(){
var current = head;
var string = '';
while(current){
string += current.element;
current = current.next;
}
return string;
};
//函数的打印,将其转换为一个数组输出,与toString方法类似
this.print=function(){
var current = head;
var array = [];
while(current){
array.push(current.element);
current = current.next;
}
return array;
};
}
常见链表题
从尾到头打印链表值
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)——《剑指offer》题06
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
//用一个队列来存储打印结果,从队列头部插入
var reversePrint = function(head) {
let array = [];
while(head){
array.unshift(head.val);
head = head.next;
}
return array;
};
//也可以利用栈的特点,将节点值存入到栈中,然后进行反转
var reversePrint = function(head){
let array = [];
while(head){
array.push(head.val);
head = head.next;
}
return array.reverse();
}
反转链表
反转一个单链表,可以使用迭代或递归—《剑指offer》题24
//迭代写法
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
var reverseList = function(head){
if(head == null || head.next == null){return head;}
let pre = head;
let temp = null;
let cur = head.next
while(cur != null){
temp = cur.next;
pre.next = cur.next;
cur.next = head;
head = cur;
cur = temp;
//[head, cur] = [cur, temp]; //解构赋值写法
}
return head;
}
递归写法图解
//递归写法
var reverseList = function(head){
return reverse(null, head);
}
function reverse(pre, cur){
if(cur == null){return pre};
let temp = cur.next;
cur.next = pre;
return reverse(cur, temp);
}
合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的——《剑指offer》题25
//迭代写法
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
var mergeTwoList = function(l1, l2){
if(!l1){return l2;}
if(!l2){return l1;}
let temp = new ListNode();
let head = temp; //保存头节点
while(l1 && l2){
if(l1.val < l2.val){
temp.next = l1;
l1 = l1.next;
}else{
temp.next = l2;
l2 = l2.next;
}
temp = temp.next;
}
//当l1或l2为空时,跳出while循环,并将不为空的链表添加到temp.next
if(!l1){temp.next = l2;}
if(!l2){temp.next = l1;}
return head.next; //由于head头节点没有值,因此是返回head.next
}
//递归写法
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
var mergeTwoList = function(l1, l2){
if(!l1){return l2;}
if(!l2){return l1;}
if(l1.val < l2.val){
l1.next = mergeTwoList(l1.next, l2);
return l1;
}else{
l2.next = mergeTwoList(l1, l2.next);
return l2;
}
}
链表倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点—《剑指offer》题22
//自己的写法,比较麻烦:先得到链表的长度,然后再得到顺数第len-k个节点,即为所求节点
var getKthFromEnd = function(head, k) {
if(!head || !k){return null;}
let len = 0;
let temp = head;
while(temp){
temp = temp.next;
len++;
}
let pos = len - k;
let index = 0;
while(index != pos){
head = head.next;
index++;
}
return head;
};
//参考别人的写法:用两个指针,一个先走k步,然后两个指针一块走,当前一个指针到达终点时,后一个节点所在位置即为所求
var getKthFromEnd = function(head, k) {
if(!head || !k){return null;}
let font = head, behind = head;
let index = 0;
while(font){
if(index >= k){
behind = behind.next;
}
font = font.next;
index++;
}
return index < k ? null : behind;
}
链表中环的入口节点
给定一个链表,若其中包含环,请找出该链表的环的入口节点,否则输出null—《剑指offer》题23
思路:
- 判断链表是否有环:设置两个指针p1,p2,从头节点出发,p1走两步,p2走一步,如果能相遇,说明链表有环;
- 从环内的某个节点开始计数,再回到此节点时得到链表环的长度len;
- p1,p2回到头节点,让p1走len步,当p2与p1相遇时即为链表环的起点
function EntryNodeOfLoop(head){
if(!head || !head.next){return null;}
let p1 = head.next, p2 = head.next.next;
//判断是否有环,无环返回null,有环则p1 == p2
while(p1 != p2){
if(!p2 || !p2.next){return null;}
p1 = p1.next;
p2 = p2.next.next;
}
//获取环的长度len,p1已在环内
let temp = p1;
let len = 1;
p1 = p1.next;
while(p1 != temp){
p1 = p1.next;
len++;
}
//找到入口节点
p1 = p2 = head;
while(len-- > 0){
//p2先走len个长度
p2 = p2.next;
}
while(p1 != p2){
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共节点—《剑指offer》题52
思路:
- 先找到两个链表的长度len1,len2
- 让长一点的链表先走len2-len1步,使得两个链表距离链表尾部的长度相同;
- 两个链表一起走,并比较节点是否相同,相同即为公共节点,判断是否相同是需要值相同,并下一个节点也相同
var getIntersectionNode = function(headA, headB) {
if(!headA || !headB){return null;}
let len1 = getLength(headA);
let len2 = getLength(headB);
let long, short, gap;
//判断哪个链表长度更长
if(len1 > len2){
long = headA;
short = headB;
gap = len1 - len2;
}else{
long = headB;
short = headA;
gap = len2 - len1;
}
//让长链表先走gap步
while(gap-- > 0){
long = long.next;
}
//两个链表一起走,判断第一个公共节点
while(long){
if(long === short){
return long;
}
long = long.next;
short = short.next;
}
return null;
};
function getLength(head){
let cur = head;
let len = 0;
while(cur){
len++;
cur = cur.next;
}
return len;
}
//另一种解法-先让p1在headA中走完,然后跳转到headB头部;同时p2在headB中走,走完跳转到headA的头部,当p1与P2相等时,就是公共节点
var getIntersectionNode = function(headA, headB) {
let p1 = headA, p2 = headB;
while(p1 || p2){
if(p1 === p2){
return p1;
}
if(p1){
p1 = p1.next;
}else{
p1 = headB;
}
if(p2){
p2 = p2.next;
}else{
p2 = headA;
}
}
return null;
}
圈圈中最后剩下的数字-(约瑟夫环)
0,1,...,n-1
这n
个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m
个数字。求出这个圆圈里剩下的最后一个数字。 —《剑指offer》题62
//数组方法
var lastRemaining = function(n, m) {
if(n < 1 || m < 1){return -1;}
let array = [];
let index = 0;
for(let i = 0; i < n; i++){
array[i] = i;
}
while(array.length > 1){
//计算每次删除元素的下标
index = (index + m) % array.length - 1;
if(index >= 0){
array.splice(index, 1);
}else{
array.splice(array.length - 1, 1);
index = 0;
}
}
return array[0];
}
//用链表来模拟环
//记录头节点的前一个节点cur,以保证找到要删除的节点是cur.next
//每次循环m次找到目标节点删除,直到链表只剩下一个节点
//该方法会超时
var lastRemaining = function(n, m) {
if(n < 1 || m < 1){
return -1;
}
const head = {val:0};
let cur = head;
for(let i = 1; i < n; i++){
cur.next = {val:i};
cur = cur.next;
}
cur.next = head; //构成环
while(cur.next != cur){
for(let i = 0;i < m-1; i++){
cur = cur.next;
}
cur.next = cur.next.next;
}
return cur.val;
}
//数学的递归写法
//推导公式:f(n,m) = (f(n-1,m)+m)%n
//该方法爆栈
var lastRemaining = function(n, m) {
if(n < 1 || m < 1){
return -1;
}else{
return mathRecursion(n,m);
}
};
function mathRecursion(n,m){
if(n === 1){
return 0;
}
return (mathRecursion(n-1, m)+m)%n;
}
//迭代写法
var lastRemaining = function(n, m) {
let result = 0;
for(let i = 2;i <= n;i++){
result = (m + result) % i;
}
return result;
}
删除链表中的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。—《剑指offer》题18
//时间复杂度为O(n)
var deleteNode = function(head, val) {
if(!head){return null;}
let pre = new ListNode();
pre.next = head;
let cur = pre;
while(cur.next){
if(cur.next.val === val){
cur.next = cur.next.next;
break;
}else{
cur = cur.next;
}
}
return pre.next;
}
删除表中重复的节点
移除未排序链表中的重复节点。保留最开始出现的节点 。
思路:利用数组的下标来存储节点的值,并将元素值设置为1,然后当遇到相同的值时,就会重复索引,再将该节点删除
var removeDuplicateNodes = function(head) {
let arr = [];
let pre = null;
let cur = head;
while(cur != null){
if(arr[cur.val] != undefined){
pre.next = cur.next;
cur = cur.next;
}else{
arr[cur.val] = 1;
pre = cur;
cur = cur.next;
}
}
return head;
}
复杂链表的复制
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head —《剑指offer》题35
思路:
- 第一次循环复制每个节点和next指针,并保存为原节点-复制节点的映射关系
- 第二次循环,通过哈希表获得节点对应的复制节点,更新random指针
/**
* // Definition for a Node.
* function Node(val, next, random) {
* this.val = val;
* this.next = next;
* this.random = random;
* };
*/
var copyRandomList = function(head) {
if(!head){return null;}
const map = new Map();
let cur = head; //当前节点
const newHead = new Node(cur.val);
let newCur = new newHead; //当前节点的复制节点
map.set(cur, newCur);
//第一次循环,复制每个节点的值和指针
while(cur.next){
newCur.next = new Node(cur.next.val);
cur = cur.next;
newCur = newCur.next;
map.set(cur, newCur);
}
//第二次循环
newCur = newHead;
cur = head;
while(newCur){
newCur.random = map.set(cur.random);
newCur = newCur.next;
cur = cur.next;
}
return newCur;
}