链表中的元素在内存中不是顺序存储的,而是通过next指针联系在一起的。常见的链表有单向链表、双向链表、环形链表等
在 JavaScript 刷题中涉及链表的算法有很多,常见的包括:
1. 遍历链表:从头到尾遍历链表,处理每个节点的值或指针。
2. 反转链表:将链表的指针方向反转,使链表的尾部变为头部。
3. 合并两个有序链表:将两个有序链表合并成一个有序链表。
4. 删除链表中的指定节点:删除链表中特定值或位置的节点。
5. 检测链表中是否有环:判断链表中是否存在环形结构。
对于新手刷题链表,一些需要注意的方法和建议包括:
1. 熟悉链表的基本操作:了解链表的基本结构和操作,包括节点的定义、指针操作等。
2. 注意边界情况:在处理链表时,要考虑边界情况,如空链表、只有一个节点的链表等。
3. 使用递归:递归是解决链表问题的常见方法,可以简化问题的复杂度。
4. 双指针技巧:在解决链表问题时,双指针技巧经常会派上用场,例如快慢指针用于检测环。
5. 利用辅助数据结构:有时候可以借助辅助数据结构如哈希表来解决链表问题。
206. 反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
思路:新创建一个链表,链表头指针pre,新的链表用pre指向,最后返回这个头指针。
1——》2——》3——》4——》5——》null
可以转换为
null《——1《——2《——3《——4《——5
pre初始是空对象pre = null
难点来了
怎么实现null《——1 让1的next指向null呢?
如果1的next改变指向,那么2以及之后的数据将丢失,所以需要提前将1的next暂存给一个变量;
然后这个时候可以改变1的next指向为pre了。
那原始链表还需要遍历呀,这个时候将暂存的next赋给current。同时pre也从null后移指向1.所以第二次遍历结束
2——》3——》4——》5——》null
^
current指向2
null《——1
^
pre指向
第三次遍历结束
3——》4——》5——》null
^
current指向3
null《——1《——2
^
pre指向
这道题可能当时做会了过段时间就忘了,所以要牢记改变指向的时候数据连接会丢失,需要考虑丢掉还是要保留,如果要保留数据,一定在改变指向前处理。
/**
* 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 current = head;
while (current !== null) {
let nextNode = current.next;
current.next = prev;
prev = current;
current = nextNode;
}
return prev;
};
使用迭代的方式遍历链表,将每个节点的指针方向反转。使用
prev
指针来保存已经反转部分的链表,current
指针来遍历原始链表。在每一步中,我们将current.next
指向prev
,然后更新prev
和current
指针继续向后移动,直到遍历完整个链表。最后,返回反转后的链表的头节点,即
prev
指针所指向的节点,这个节点就是原始链表的尾节点,也就是反转后链表的头节点。
237. 删除链表中的节点
有一个单链表的 head
,我们想删除它其中的一个节点 node
。
给你一个需要删除的节点 node
。你将 无法访问 第一个节点 head
。
链表的所有值都是 唯一的,并且保证给定的节点 node
不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:
- 给定节点的值不应该存在于链表中。
- 链表中的节点数应该减少 1。
node
前面的所有值顺序相同。node
后面的所有值顺序相同。
思路:不删除当前node节点,又没有上一个元素的信息,不能更改上一个元素指向node.next。从node出发,让node变成node.next,跳过node.next这个节点。我变成了他,同时,让他消失。。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @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;
};
node.val = node.next.val让node替换成node.next元素
node.next = node.next.next 干掉node.next
node替代node.next结束
83. 删除排序链表中的重复元素
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
/**
* 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) {
if (!head) return head;
let cur = head;
while (cur.next) {
if (cur.val === cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
};
思路:比较简单的链表删除,题目已经保证是排序好的链表,只用考虑相邻元素重复的情况。当前元素和下一个元素重复的话,将当前元素的next指向,下下一个元素。跳过重复的。
如果没有重复,指针往后移动一个。注意需要返回排序后的数组,所以头指针的指向不能变,最后返回head指针。过程用cur指针替代head进行向后移动。
203. 移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1 输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7 输出:[]
思路:这道题诈看很简单,用指针遍历,当前元素和val相等时,用下一个元素替换当前元素。这种方法在最后一个元素时会失效,因为无法杀死最后一个元素了,最后一个元素和它前一个连接无法断开。
那如果在头节点之前新增一个节点呢,用cur的next节点参与比较。当遇到最后一个元素时,其实是示例中的5.next ===6,让5.next=null即可,杀死了最后一个元素6
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function (head, val) {
let newNode = new ListNode(0, head);
let cur = newNode;
while (cur) {
if (cur.next && cur.next.val === val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return newNode.next;
};
这里用newNode指向新创建的链表,最后返回的是newNode.next,因为头加入了一个无用的节点。
用next节点参与比较,当前元素改变它的next指向即可
二刷:可以使用一个虚拟头结点,将head作为虚拟头结点的next节点,这样就可以处理head.val==val的情况了
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function (head, val) {
let dummyNode = new ListNode(0, head);//新建一个虚拟头结点删除链表元素
let cur = dummyNode;
while (cur.next) {
if (cur.next.val == val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return dummyNode.next;
};
876. 链表的中间结点
给你单链表的头结点 head
,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
方法一:首先遍历一遍单链表,记录链表的长度len,计算中间节点的位置。 用空间换时间:
/**
* 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 middleNode = function (head) {
let cur = head;
let setp = Math.floor(getLength(head) / 2);
while (setp--) {
cur = cur.next;
}
return cur;
};
function getLength(head) {
let cur = head;
let length = 0;
while (cur) {
length++;
cur = cur.next;
}
return length;
}
当我用这种方法写完,时间复杂度n+n/2感觉没那么简单,一看评论区,果然大神就是多,双指针,时间复杂度为n,一趟排序就找到中间位置了。
方法二:利用快慢指针,快指针每次走两步,慢指针每次走一步,所以快指针走的距离为慢指针的两倍,故当快指针遍历到链表末尾时,慢指针指向记为中间节点
/**
* 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 middleNode = function (head) {
let f = head;
let s = head;
while (f && f.next) {
f = f.next.next;
s = s.next;
}
return s;
};
简直了,只要思想简单,代码就简单。这里判断f.next.next最后一步是等于null所以f.next不能为null,f也不能为null。
1290. 二进制链表转整数
给你一个单链表的引用结点 head
。链表中每个结点的值不是 0 就是 1。已知此链表是一个整数数字的二进制表示形式。
请你返回该链表所表示数字的 十进制值 。
思路:我原称这是一道数学题
让2进制转10进制,头节点是2进制的高位,尾节点是2进制的低位,一般可以将链表翻转,从低位开始用x*2的length次方逐步求和。但是也可以从高位到低位逐步求和。怎么从高位到低位呢
示例:1011 =11
第一个元素 1=result
到第二个元素,知道长度为2 将上一个结果*2+当前元素0=2=result
到第三个元素,知道长度为3,前两个都少个2,由于2是公共乘数,可以将2提出来,2*result+1=5=result
到第四个元素,知道长度为4,同理2*result+1=11=result
结束,每个缺失的2都补上了
/**
* 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 {number}
*/
var getDecimalValue = function (head) {
let result = 0;
let cur = head;
while (cur) {
result =( result << 1) + cur.val;
cur = cur.next;
}
return result;
};
result<<1可以用result*2表示。
这里要将位运算用括号包裹起来,在 JavaScript 中,位运算符(如左移运算符
<<
)的优先级比加法运算符低。因此,如果在一个表达式中同时使用位运算符和加法运算符,为了确保运算顺序正确,建议使用括号来明确指定运算的顺序。
61. 旋转链表
给你一个链表的头节点 head
,旋转链表,将链表每个节点向右移动 k
个位置。
思路:将尾部向前数第K个元素作为头,原来的尾接到原来的头上 。整个过程就变成了找四个节点。旧的尾节点end,从前往后数第len-k是新的尾节点newEnd,新的尾结点后面一个是新的头节点newHead。然后将end->head。将newEnd=null。然后返回newHead就好了
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var rotateRight = function (head, k) {
//边界处理
if (!head || !head.next || k == 0) return head;
let kk;
let cur = head;
let end;
let newEnd, newHead;
let len = 0;
//让cur往后走,找到最后一个节点end
while (cur.next) {
len++;
cur = cur.next;
}
end = cur;
len++;
kk = k % len;
if (kk == 0) return head;
//cur归位到head,找到第len-k个节点,这是新的尾结点
cur = head;
for (let i = 1; i < len - kk; i++) {
cur = cur.next;
}
newEnd = cur;
//尾结点的下一个是新的头结点
newHead = cur.next;
//让end指向head,连接旧的尾和旧的头
end.next = head;
//断开newEnd,让newEnd指向null
newEnd.next = null;
return newHead;
};
快慢指针
141. 环形链表
如果链表中存在环,快指针和慢指针最终会在环中相遇;如果链表中不存在环,快指针会先到达链表尾部。
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
使用快慢指针的技巧来检测链表中是否存在环。具体来说,定义了两个指针
f
和s
,初始时都指向链表的头部。然后,使用一个循环来遍历链表,其中快指针f
每次移动两步,慢指针s
每次移动一步。如果链表中存在环,快指针f
会在某个时刻追上慢指针s
,此时就可以判断链表中存在环,返回true
;如果快指针f
到达链表尾部(即f
或f.next
为null
),则说明链表中不存在环,返回false
。这种算法的时间复杂度为 O(n),其中 n 是链表的长度。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function (head) {
let f = head, s = head;
while (f != null && f.next != null) {
s = s.next;
f = f.next.next;
if (s == f) return true;
}
return false;
};
使用双等号
==
运算符来比较两个对象并不会直接比较它们的内容,而是比较它们的引用。s==f则快指针和慢指针指向了同一个对象
142. 环形链表 II
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思路:这题题目既不希望你通过哈希表存储已遍历节点,也不希望你更改链表本身的val值。而是希望你只根据快慢指针以及数学思维解决问题。
就是一道脑筋急转弯了。如果做题不看题解根本算不明白。
看下代码随想录对这段的题解吧:
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
相遇的时候slow指针行走的步数=x+y+(y+z)*m圈,假设慢指针在环里走了m圈。
fast指针行走的步数=x+y+(y+z)*n圈,假设快指针在环里走了n圈。
fast比slow多走一倍,因此2(x+y)+2(y+z)*m=x+y+(y+z)*n
x+y=(n-2m)(y+z)
x=(n-2m-1)(y+z)+z
看x和z的关系。其中y+z是环的周长,可以看到,x长度等于慢指针绕了环n-2m-1圈后,在走z个单位就可以与x重合。因此,可以让slow和头指针同步走,两者一定会相交,如果环很小,slow会绕环n-2m-1圈。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function (head) {
if (!head || !head.next) return null;
//快慢指针,如果相交即有环
let slow = head, quick = head;
while (quick && quick.next) {
slow = slow.next;
quick = quick.next.next;
if (slow === quick) {//有环
let cur = head;
while (cur != slow) {//相加的时候,慢指针和头指针同时走c个节点一定能相遇
cur = cur.next;
slow = slow.next;
}
return cur;
}
}
return null;
};
19. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
思路:快慢指针,快指针先走n步,然后快慢一起走,直到快指针走到最后。
要进行边界值的判断,题目说了n一定大于1,因此,如果节点数小与等于1返回的一定是null。同时,如果快指针初始化走n步就已经走完了,说明删除的是头节点,此时返回头节点下一个即可。否则删除的是中间的元素。n是小与等于链表长度sz的,所以初始化快指针一定能顺利走完链表
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function (head, n) {
if (!head || !head.next) return null;
//让快指针先走n+1步
let slow = head, quick = head;
for (let i = 0; i < n; i++) {//slow记录要删除的节点的前一个节点
quick = quick.next;
}
if (!quick) {//如果快指针走n步的时候就已经到结束位置了,删除的是头节点
return head.next;
}
while (quick.next) {
slow = slow.next;
quick = quick.next;
}
slow.next = slow.next.next;
return head;
};
当然,这个题还可以维护一个n+1长度的队列,如果队列长度等于n说明删除的是头节点,否则,队列的头维护的是要删除节点的前一个节点。
链表之间比较
21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function (list1, list2) {
if (!list1) return list2;
if (!list2) return list1;
let head = new ListNode(0, null);
let cur = head;
while (list1 && list2) {
let val;
if (list1.val < list2.val) {
val = list1.val;
list1 = list1.next;
} else {
val = list2.val;
list2 = list2.next;
}
let node = new ListNode(val, null);
cur.next = node;
cur = node;
}
cur.next = list1 || list2;
return head.next;
};
为了正确地创建 ListNode 的实例,应该使用 new ListNode(val, null)而不是ListNode(val,null)这是因为创建的是节点的实例而不是单纯的调用函数。这里的ListNode是构造函数。
怎么区分普通函数和构造函数?
- 看大小写:一般构造函数函数名的首字母会大写,普通函数是小写。
- 函数中的this指向不同:普通函数的this在严格模式下指向undefined,非严格模式指向window对象。构造函数的this指向它创建的对象实例。
构造函数的执行流程
- 立即创建一个新的对象
- 将新建的对象设置给函数中的this,在构造函数中可使用this来引用新建的对象
- 逐行执行函数中的代码
- 将新建的对象作为返回值返回
160. 相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
这道题,如果不看题解,第一次做肯定没有思路
首先比较的不是节点的值相等,而是指向同一个节点。
其次,一般会考虑n^2时间复杂度,穷举A,对A的每项在穷举B
其实只要找到A、B的起始位,让A和B可以同步向后 移动。
例如,上图中,如果B一开始指向b2,那么比较b2和a1,不相等,均向后移动一位,比较a2和b3是否相等。不等,在往后移动,比较c1和c1是否相等,相等则返回A或B当前指向的节点
要找出两个单链表相交的起始节点,可以使用双指针法。具体步骤如下:
- 分别遍历两个链表,计算它们的长度。
- 计算两个链表的长度差,然后让较长的链表的指针先移动长度差个节点,使得两个链表剩余的长度相等。
- 同时遍历两个链表,比较节点是否相同,找到相交的起始节点。
/**
* 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 curA = headA;
let curB = headB;
let lengthA = getLength(curA);
let lengthB = getLength(curB);
if (lengthB > lengthA) {
let step = lengthB - lengthA;
while (step--) {
curB = curB.next;
}
} else if (lengthB < lengthA) {
let step = lengthA - lengthB;
while (step--) {
curA = curA.next;
}
}
while (curA) {
if (curA == curB) {
return curA;
} else {
curA = curA.next;
curB = curB.next;
}
}
return null;
};
function getLength(head) {
let length = 0;
let current = head;
while (current) {
length++;
current = current.next;
}
return length;
}
注意在计算链表长度时不要破坏传入的head指针指向,要创建新的变量指向head指针。
拿到lengthA和lengthB后可以判断哪个更长,移动长的链表的cur指针。
A和B的cur指针同步移动时无论判断while(curA)还是while(curB)都可以,因为剩余要比较的链表长度相等呀,比较次数也相等
当A和B的cur指向一样,同理,返回curA或curB都行,结束判断
2. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
思路:考察两个链表进行相加,比较经典的题目。在比较的时候要考虑两种情况,两个链表都存在,以及剩余了一个链表,其中一个比较长。并且要考虑最后一个进位,要单独处理。由于创建一个新的链表返回,所以新创建的头结点不能动,要用额外的地址指向新的头结点,并移动额外的变量。最后返回头结点。
/**
* 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 head = new ListNode();
let cur = head;//让cur初始时指向head,过程中只移动cur
let carry = 0;//进位值
let sum = 0;//sum值
while (l1 && l2) {//l1和l2都存在
sum = l1.val + l2.val + carry;//三者相加
carry = Math.floor(sum / 10);//拿到进位值
const node = new ListNode(sum % 10);//生成新的节点
cur.next = node;//将新的节点插入
cur = cur.next;//cur指向最后一个节点
l1 = l1.next;
l2 = l2.next;
}
let link = l1 || l2;//上面结束时,要么l1多,要么l2多出来
while (link) {//用link计算多出来的链表
sum = link.val + carry;//不要忘了进位
const node = new ListNode(sum % 10);
carry = Math.floor(sum / 10);
cur.next = node;
cur = cur.next;
link = link.next;
}
if (carry) {//不要忘了进位
const node = new ListNode(1);
cur.next = node;
cur = cur.next;
}
return head.next;//由于第一个head是空,所以返回head.next
};
24. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路:这题为什么是中等题,这是简单题吧,就是两个指针一个在后,一个在前,两两交换val值。往前走的时候每次都两步就好啦。
/**
* 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 swapPairs = function (head) {
if (!head || !head.next) return head;
//初始快慢指针
let fast = head.next;
let slow = head;
//如果快指针还没到头
while (fast) {
[fast.val, slow.val] = [slow.val, fast.val];//交换快慢指针的val
slow = fast.next;//慢指针指向fast后面
fast = fast.next ? fast.next.next : null;//先判断fast.next是不是存在,fast往前走两步
}
return head;//交换后返回head
};
86. 分隔链表
给你一个链表的头节点 head
和一个特定值 x
,请你对链表进行分隔,使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
思路:猛一看我还以为二分查找呢,然后看了一下,保留节点出现顺序。那么就想到了重建链表。根据x,遍历一次链表,将比x小的构建一条链表,大于等于x的构建一个链表,采用尾插法插入。然后将两个链表拼接,小的在前,大的在后。这里除了新建的两个链表创建节点外。其他节点用原有链表里的,重点在改变链表之间的指向关系
由于头节点是一个空节点,所以返回的是头节点的next
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} x
* @return {ListNode}
*/
var partition = function (head, x) {
if (!head || !head.next) return head;
//比x大的串成一个链表l2
//比x小的串成一个链表l1
//这两个链表合并l1->l2就是要得到的结果
let smallHead = new ListNode();
let curS = smallHead;
let largeHead = new ListNode();
let curL = largeHead;
let cur = head;
while (cur) {
if (cur.val < x) {//找到比x小的节点
curS.next = cur;
curS = curS.next;
} else {
curL.next = cur;
curL = curL.next;
}
cur = cur.next;//让原链表cur向后走
}
curL.next = null;
curS.next = largeHead.next;
return smallHead.next;
};
链表结合其他数据类型
705. 设计哈希集合
不使用任何内建的哈希表库设计一个哈希集合(HashSet)。
实现 MyHashSet
类:
void add(key)
向哈希集合中插入值key
。bool contains(key)
返回哈希集合中是否存在这个值key
。void remove(key)
将给定值key
从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。
思路:这道题需要有哈希表的前置储备知识,不然你都不知道它想考你什么
实现哈希表(也称为哈希映射或散列表)的方式有很多种
- 数组+线性探测实现:将哈希值转换为数组的索引,将元素存储在数组中。当发生哈希冲突时,尝试使用下一个索引来存储元素,直到找到一个空闲的位置。这种实现方式简单,但会发生聚集现象,从而导致哈希表的性能下降。
- 数组+链表:用数组来存储哈希表中的元素,但当发生哈希冲突时,不将元素存储在数组中,而是将元素存储在链表中。具体来说,将哈希值转换为数组的索引,并将元素存储在该索引对应的链表中。但如果链表的长度过长,则在查找、插入和删除元素时的性能会下降。
- 双散列实现:双散列实现是一种开地址实现的扩展。在这种实现方式中,我们使用两个不同的哈希函数来计算元素的索引。当发生哈希冲突时,我们会尝试使用第二个哈希函数来计算索引,从而避免了使用相同的索引来存储不同的元素。
在本题的实现里,采用数组+链表的方式存储key。找数组下标的方法就用key%数组的长度。找到数组index后,将key放在index对应的链表里。于是对哈希表的判断元素是否存在,添加元素、删除元素,就转换成了在单链表里查找、删除、新增了。只不过多了找头结点的过程
// 额外定义链表节点类
class Node {
constructor(val, next) {
this.val = val !== undefined ? val : -1;
this.next = next != undefined ? next : null;
}
}
var MyHashSet = function () {
//设定哈希表容量
capacity = 100;
this.data = [];//存储key对应的单链表
//初始化每个index的节点
for (let i = 0; i < capacity; i++) {
this.data[i] = new Node();
}
//将key转换为index,获取index存储链表的头节点
this.getHead = key => {
let index = key % capacity;
return this.data[index];
}
};
/**
* @param {number} key
* @return {void}
*/
MyHashSet.prototype.add = function (key) {
if (this.contains(key)) return;
//找到key通过hash生成的index里的链表头节点
let head = this.getHead(key);
//插在头结点的后面
let temp = new Node(key, head.next);
head.next = temp;
};
/**
* @param {number} key
* @return {void}
*/
MyHashSet.prototype.remove = function (key) {
//先判断集合里有没有key
if (!this.contains(key)) return;
//有key,找到对应index的单链表头节点
let head = this.getHead(key);
//单链表中删除指定key,都会吧
while (head.next && head.next.val != key) {
head = head.next;
}
head.next = head.next.next;
};
/**
* @param {number} key
* @return {boolean}
*/
MyHashSet.prototype.contains = function (key) {//先写contains方法,因为新增和删除都会用到
//根据key去找对应数组下标的链表头节点
let head = this.getHead(key);
//在单链表里查找key,都会吧
while (head && head.val != key) {
head = head.next;
}
return head != null;
};
/**
* Your MyHashSet object will be instantiated and called as such:
* var obj = new MyHashSet()
* obj.add(key)
* obj.remove(key)
* var param_3 = obj.contains(key)
*/
706. 设计哈希映射
不使用任何内建的哈希表库设计一个哈希映射(HashMap)。
实现 MyHashMap
类:
MyHashMap()
用空映射初始化对象void put(int key, int value)
向 HashMap 插入一个键值对(key, value)
。如果key
已经存在于映射中,则更新其对应的值value
。int get(int key)
返回特定的key
所映射的value
;如果映射中不包含key
的映射,返回-1
。void remove(key)
如果映射中存在key
的映射,则移除key
和它所对应的value
。
思路:与705一样,节点多了一个属性而已
使用数组+单链表的方式构建哈希表。通过key%数组得到哈希索引,在根据索引找到单链表的头节点。这样get、put和remove就转换为在单链表里进行查找、新增和删除
//定义单链表节点类,key,val,next
class Node {
constructor(key, val, next) {
this.key = key != undefined ? key : -1;
this.val = val != undefined ? val : -1;
this.next = next != undefined ? next : null;
}
}
var MyHashMap = function () {
let capactity = 100;//取哈希的数组长度
this.data = [];//存单链表节点的数组
for (let i = 0; i < capactity; i++) {
this.data[i] = new Node();
}
//获取key所在单链表头节点
this.getHead = key => {
let index = key % capactity;
return this.data[index];
}
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
MyHashMap.prototype.put = function (key, value) {
//先调get方法,判断是否存在
let head = this.getHead(key);
if (this.get(key) == -1) {//不存在,头插法插入这个节点
let node = new Node(key, value, head.next);
head.next = node;
} else {//存在,单链表遍历,修改节点val
while (head && head.key != key) {
head = head.next;
}
head.val = value;
}
};
/**
* @param {number} key
* @return {number}
*/
MyHashMap.prototype.get = function (key) {//最先实现get方法,因为put和remove都会用到
let head = this.getHead(key);//获取单链表头节点
while (head && head.key != key) {//单链表里查找元素
head = head.next;
}
return head ? head.val : -1;
};
/**
* @param {number} key
* @return {void}
*/
MyHashMap.prototype.remove = function (key) {
if (this.get(key) == -1) return;//调get方法,看有没有key
let head = this.getHead(key);//有的话,找到head节点
while (head.next && head.next.key != key) {//单链表节点删除
head = head.next;
}
head.next = head.next.next;
};
/**
* Your MyHashMap object will be instantiated and called as such:
* var obj = new MyHashMap()
* obj.put(key,value)
* var param_2 = obj.get(key)
* obj.remove(key)
*/