933. 最近的请求次数
- 有新请求就入队,3000ms前发出的请求出队
- 队列长度就是最近请求次数
/*
时间复杂度:有while循环体,时间复杂度是O(n),n就是被踢出去的请求个数
空间复杂度 O(n):设置了数组this.q,队列的长度是请求个数,n
*/
var RecentCounter = function(){
this.q = []
}
RecentCounter.prototype.ping=function(t){
this.q.push(t) // 每次发起请求,把新请求入队
while(this.q[0]<t-3000){ // 小于表示不在范围内,大于才或等于才正常
this.q.shift() // 把队头不在[t-3000, t]这个时间范围内的老请求就踢出去
}
return this.q.length
}
const a = new RecentCounter()
a.ping([])
a.ping([1])
a.ping([100])
a.ping([3001])
a.ping([3002])
2.两数相加
思路:遍历两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加
/** 时间复杂度:有while循环体,所以是O(n),循环次数n是l1、l2链表长度的较大值
* 空间复杂度:有新造的链表l3,这个链表的长度也可能是两个链表l1、l2中较长的链表长度,还有进一的例子(较长链表再加一位),O(n),n为两链表较长的长度
*/
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
var addTwoNumbers = function(l1, l2){
const l3 = new ListNode(0) // 新链表
let p1 = l1
let p2 = l2
let p3 = l3 // 往新链表追加元素时,需要一个指针不停指向最后一个节点,才能在最后一个节点进行追加
let carry = 0 // 十位上的数,留到下一轮相加
while(p1 || p2){
const v1 = p1? p1.val:0; // l1、l2链表有长有短,为空就为0
const v2 = p2? p2.val:0;
const val = v1+v2+carry // 加上上一轮带过来的carry
carry = Math.floor(val/10) // 获取十位上的数
p3.next = new ListNode(val%10) // 获取个位上的数
// 判断有没有值,没值就不能调用.next,有值就把指针移到下一位
if(p1) p1 = p1.next
if(p2) p2 = p2.next
p3 = p3.next
}
// 循环体结束后,判断最后一个是否进一了,有的话就追加到新链表上
if(carry) p3.next = new ListNode(carry)
return l3.next // 空结点(0)的后面,所以next
}
★83.删除排序链表中的重复元素
- 因为链表是有序的,所以重复元素一定相邻
- 遍历链表,如果发现当前元素和下一个元素值相同,就删除下一个元素值
- 遍历结束后,返回原链表的头部
/** 时间复杂度:O(n) while循环体,n为链表长度
* 空间复杂度:没有额外的存储,所以为O(1)
*/
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
};
★82.删除排序链表中的重复元素 II(不留独苗,哨兵结点)
跟上面的区别是不留独苗,即把前驱和后继一起删除
链表的第一个结点,因为没有前驱结点,所以需要哨兵结点(处理头结点为空的边界问题)
遇到值相同的相邻节点时,不断往前遍历,直到找到值不相等的相邻节点,将初始位置的上一个节点的next指针指向现在遍历到的结点位置
const deleteDuplicates = function(head) {
// 极端情况:0个或1个结点,则不会重复,直接返回
if(!head || !head.next) {
return head
}
// dummy 登场
let dummy = new ListNode()
// dummy 永远指向头结点
dummy.next = head
// cur 从 dummy 开始遍历
let cur = dummy
// 当 cur 的后面有至少两个结点时
while(cur.next && cur.next.next) {
// 对 cur 后面的两个结点进行比较
if(cur.next.val === cur.next.next.val) {
// 若值重复,则记下这个值
let val = cur.next.val
// 反复地排查后面的元素是否存在多次重复该值的情况
while(cur.next && cur.next.val===val) {
// 若有,则删除
cur.next = cur.next.next
}
} else {
// 若不重复,则正常遍历
cur = cur.next
}
}
// 返回链表的起始结点
return dummy.next;
}
https://github.com/Alex660/Algorithms-and-data-structures
剑指Offer 18.删除链表的节点(删除指定节点)
var deleteNode = function(head, val) {
if (head == null) return null;
if (head.val == val) return head.next;
let p = head;
while (p.next != null && p.next.val != val) {
p = p.next;
}
if (p.next != null) {
p.next = p.next.next;
}
return head;
};
237.删除链表中的节点(同上)
- 无法直接获取被删出节点的上一个节点
- 将被删除节点转移到下一个节点
1)将被删除节点的值改为下个节点
2)删除下一个节点
比如删除9,先把9赋值到节点1上,把最后一个9删掉,就变相删除节点1
var deleteNode=function(val){
node.val=node.next.val
node.next = node.next.next
}
★判断链表是否有环
- 方法1:设置flag
- 暴力求解,遍历链表,用哈希表Map或Set记录访问过的结点,后面看在新访问的元素是否之前存在Map/Set里面,表示又走回到原来的老结点去
- 直接在head上定义属性flag
var hasCycle = function(head) {
let map = new Map()
while(head!=null){
if(map.has(head)){
return true
}else{
map.set(head, head)
}
head = head.next
}
return false
}
// flag
// 入参是头结点
const hasCycle = function(head) {
// 只要结点存在,那么就继续遍历
while(head){
// 如果 flag 已经立过了,那么说明环存在
if(head.flag){
return true
}else{
// 如果 flag 没立过,就立一个 flag 再往
下走
head.flag = true
head = head.next
}
}
return false
}
- 方法2:快慢指针
思路:两个人在操场上起点同时起跑,速度快的人一定会和速度慢的人相遇
用一快一慢两个指针(即快指针走两步,慢指针走一步)遍历链表,如果指针能够相遇,那么链表就表示有环
其他方法:每走一个节点就存到一个数组里,下次走的话再去数组查找有没有当前节点(走n次O(n))
/** 时间复杂度:O(n),有while循环体,循环次数是n次
* 空间复杂度:o(1),没有额外的存储
*/
var hasCycle = function(head) {
let slow = head, fast = head
// 判断当前节点的next是否为null,一直判断
while(slow&&fast&&fast.next){
fast = fast.next.next
slow = slow.next
if(fast === slow){
return true
}
}
return false
}
面试题02.08.环路检测
- 设置标记判断
const detectCycle = function(head) {
while(head){
if(head.flag){
return head;
}else{
head.flag = true;
head = head.next;
}
}
return null;
}
- 快慢指针:快慢两个指针,找相遇的点。相遇后一个指针重置,改成两个慢指针。再相遇即为入口
var detectCycle = function(head) {
let slow = head, fast = head
// 判断当前节点的next是否为null,一直判断
while(slow&&fast&&fast.next){
fast = fast.next.next
slow = slow.next
if(fast === slow){
// 相遇后重置指针,再相遇即为入口
slow=head
while(slow!=fast){
slow=slow.next
fast=fast.next
}
return slow;
}
}
return null
}
链表:
- 查找:需要遍历,遍历n次就是O(n)
- 插入:找到要插入的位置,新结点的next指向要插入位置的后面结点,再把前面结点的next指向新结点 O(1)时间复杂度
- 删除:把前面结点的next指向后面结点的,相当于跨过要删除的结点,再把要删除的结点从内存中删掉 O(1)时间复杂度
双链表:既有前驱,又有后继,可以往前走,也可往后走
如何创建链表、排序、检验是否闭环
链表的快速排序比链表的堆排序使用更大
链表只会暴露头指针,所有元素是不能直接访问到的,必须通过头指针不断访问next对象才能拿到具体元素。这里的快排序和数组的快排序不一样,数组通过下标可以访问到元素,可以从左和右两边快排序,链表的不能这样做,因为拿不到最后的值
// 构造链表
class ListNode {
constructor(val) {
this.val = val
this.next = undefined
}
}
class NodeList {
constructor(arr) {
this.head = new ListNode(arr.shift())
let next = this.head
arr.forEach(item => {
next.next = new ListNode(item)
next = next.next
})
}
}
★206.反转链表
1->2->3->4 反转后 4->3->2->1
把每个结点的next指向前驱(上一个)结点,可以用递归或迭代(用三个指针)
- 方法一:reverse()
var reverseList = function(head) {
const res = []
let curr = head
while(curr !== null) {
res.unshift(curr.val)
curr = curr.next
}
return res
};
- 方法二:迭代反转
思路:定义三指针pre->cur->next,只要让cur.next = pre
就可以反转
通过三指针, 将单链表中的每个节点的后继指针指向它的前驱节点即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9oq49nju-1610543019568)(…/images/linkList/linkList_img_02.png)]
/** 时间复杂度:有while循环体,O(n)
* 空间复杂度:临时变量是单个值,没有数组,也没矩阵,所以空间复杂度是 O(1)
*/
const reverseList = function(head) {
if(!head||!head.next) return head
// 初始化前驱结点为 null
let pre = null;
// 初始化目标结点为头结点
let cur = head;
// 只要目标结点不为 null,遍历就得继续
while (cur !== null) {
// 记录一下next结点 (用next来保存后面的节点)
let next = cur.next;
// 反转指针
cur.next = pre;
// pre 往前走一步
pre = cur;
// cur往前走一步
cur = next;
}
// 反转结束后,pre 就会变成新链表的头结点
return pre
}
- 方法三:递归
1)如果当前节点是最后一个元素(curr.next == null),就让头节点指向当前节点
2)如果当前节点不是最后一个元素,即有curr.next,就递归这个next,让next变为当前节点的前驱节点,就可以让前驱节点的next指向当前节点(进行反转),当前节点的next断开设置为null
/* 递归:如果当前结点还有next,就递归反转它的next结点,让这个next结点变成当前结点的前驱节点
(前驱节点的next指向当前节点,当前节点的next断开,即设置为null)
*/
var reverseList = function(head) {
// 已经到了最后一个元素,头结点指向最后一个元素
if(head == null || head.next == null){
return head
}
// 反转上一个节点
// 如果curr还有下一个结点,递归调用reverseList(curr.next)对下一个结点反转
const curr = reverseList(head.next);
//例如,1,2,3,4,5, null
// 让下一个结点的next指向curr
head.next.next = head;
// 注意把head.next设置为null,切断4链接5的指针
// 当前结点的下一个结点设为null
head.next = null
//每层递归返回当前的节点,也就是最后一个节点。(因为head.next.next改变了,所以下一层curr变4,head变3)
return curr;
};
92.反转链表 II(局部反转)
1)定义p为游标,一直遍历到m-1的位置,赋值给节点leftHead作为缓存节点,leftHead.next就是m开始反转的位置
2)在i=m;i<n;i++
中遍历进行反转
3)处理区间节点:把缓存节点的后继节点等于反转后的第一个节点leftHead.next=pre
,反转后的最后一个节点next指向cur
// 入参是头结点、m、n
const reverseBetween = function(head, m, n) {
// 定义pre、cur,用leftHead来承接整个区间的前驱结点
let pre,cur,leftHead
// 别忘了用 dummy 嗷
const dummy = new ListNode()
// dummy后继结点是头结点
dummy.next = head
// p是一个游标,用于遍历,最初指向 dummy
let p = dummy
// p往前走 m-1 步,走到整个区间的前驱结点处
for(let i=0;i<m-1;i++){
p = p.next
}
// 缓存这个前驱结点到 leftHead 里
leftHead = p
// start 是反转区间的第一个结点
let start = leftHead.next
// pre 指向start
pre = start
// cur 指向 start 的下一个结点
cur = pre.next
// 开始重复反转动作
for(let i=m;i<n;i++){
let next = cur.next
cur.next = pre
pre = cur
cur = next
}
// leftHead 的后继结点此时为反转后的区间的第一个结点
leftHead.next = pre
// 将区间内反转后的最后一个结点 next 指向 cur
start.next=cur
// dummy.next 永远指向链表头结点
return dummy.next
}
反转相邻的链表,让两两相邻的元素进行反转
1->2->3->4 反转后 2->1->4->3
如果有第5个结点,那么第5个就不用动
排序链表
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
思路:选一个基准元素,把所有小于基准元素放左边,大于放右边;
再对基本元素左边的第一个元素作为左边的基本元素,进行上面的步骤
p指针左侧小于基准元素,p指针和q之间时大于基准元素,所以以p指针为中间线,p左侧小于,右侧大于
7大于基准元素6,p不动,q继续遍历
3小于基准元素6,3和7换位置,更新p指针
4和9也换位置,更新p指针
5和7也换位置,更新p指针
遍历完后,因为基准元素要在中间,所以p指针跟索引0的基准元素6换位置
然后把左边的进行递归,把右边的进行递归
思路:
1)找到数组的基准元素part
- 设置两个指针q、p,q不断遍历,当遍历到比基准元素小的,就跟p的下一个结点做交换swap(p.next, q),且移动p的指针
- q遍历完成后,把p位置元素和基准元素做交换swap(p, begin),让基准元素在中间
2)拿到基准元素后,递归排序基准元素左边的sort(begin, part),递归排序基准元素右边的sort(part.next, end)
// 声明链表的节点
class Node {
constructor(value) {
this.val = value
this.next = undefined
}
}
// 声明链表的数据结构
class NodeList {
constructor(arr) {
// 声明链表的头部节点
this.head = new Node(arr.shift())
// 指针
let next = this.head
arr.forEach(item => {
// next的next指向下一个结点
next.next = new Node(item)
// 把新结点作为下一个结点的父结点
next = next.next
})
}
// 交换两个节点的值
static swap = (p, q) => {
let val = p.val
p.val = q.val
q.val = val
}
// 寻找基准元素的节点
static partion = (begin, end) => {
// 基准元素
let val = begin.val
let p = begin
let q = begin.next
// 没有截止就继续遍历
while (q !== end) {
// 和基准元素做比较,比它小,就放左边
if (q.val < val) {
// 跟p的下一个结点交换
// p = p.next
// NodeList.swap(p, q)
// 或者这样写
NodeList.swap(p.next, q)
// 移动p的指针
p = p.next
}
// q不断移动
q = q.next
}
// 让基准元素跑到中间去
NodeList.swap(p, begin)
return p
}
sort(){
NodeList.sort(this.head)
let res = []
let next = this.head
while(next){
res.push(next.val)
next = next.next
}
return res
}
// 排序
static sort(begin, end) {
if (begin !== end) {
// 获取基准元素
let part = NodeList.partion(begin, end)
// 递归基准元素的左边和右边
this.sort(begin, part)
this.sort(part.next, end)
}
}
}
const nodeList = new NodeList([4, 1, 3, 2, 7, 9, 10, 12, 6])
console.log(nodeList.sort())
环形链表
// 声明链表的节点
class Node {
constructor (value) {
this.val = value
this.next = undefined
}
}
// 声明链表的数据结构
class NodeList {
constructor (arr) {
// 声明链表的头部节点
let head = new Node(arr.shift())
let next = head
arr.forEach(item => {
next.next = new Node(item)
next = next.next
})
return head
}
}
export default function isCircle (head) {
// 慢指针
let slow = head
// 快指针
let fast = head.next
while (1) {
if (!fast || !fast.next) {
return false
} else if (fast === slow || fast.next === slow) {
return true
} else {
slow = slow.next
fast = fast.next.next
}
}
}
★剑指Offer 25.合并两个排序的链表
- 迭代
比较一下哪个小就把哪个链表的头拿出来放到新的链表中(且对应的指针向后移j=j.next),一直这样循环,直到有一个链表为空,再把另一个不为空的链表挂到新的链表中
var mergeTwoLists = function(l1, l2) {
if(!l1) return l2
if(!l2) return l1
// 定义头阶段
let head = new ListNode()
let cur = head
// 指针再l1和l2直接穿梭
while(l1 && l2){
// 如果l1的节点值比较小
if(l1.val <l2.val){
// 先串起l1的节点
cur.next = l1
l1 = l1.next // l1指针先前走一步,即指向下一个l1
}else{
// l2比较小,串起l2节点
cur.next = l2
// l2向前走一步
l2 = l2.next
}
cur = cur.next // 指针向后走
}
// 处理链表不登长情况
cur.next = l1? l1:l2 // 判断还有没有l1,没有就l2
return head.next // 返回起始节点
}
const l1=new NodeList([1,2,4])
const l2=new NodeList([1,3,4])
console.log(l1)
console.log(mergeTwoLists(l1.head, l2.head))
- 递归
var mergeTwoLists = function(l1, l2) {
if (l1 === null) return l2;
if (l2 === null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
};
剑指Offer22.链表中倒数第k个节点
- 双指针求解:
第一个指针走k步,第二个指针走1步,然后同时移动,当第一个指针到达链表末尾时,返回第二个指针即可
var getKthFromEnd = function(head, k) {
var p = head, q=head
while(p){
if(k>0){ // 第一个指针先走k步
p = p.next
k--
}else{ // 再同时移动,q返回的就是结果
p = p.next
q = q.next
}
}
return q
}
const l1=new NodeList([1, 2, 3, 4, 5])
console.log(getKthFromEnd(l1.head, 2))
- 用栈解决
先push到栈里,再迭代出栈
var getKthFromEnd = function(head, k) {
let stack=[], res=[]
while(head){
stack.push(head)
head = head.next
}
while(k>0){
res = stack.pop()
k--
}
return res
}
19.删除链表的倒数第N个节点
快慢指针,快指针先走2步,快慢指针再一起走
const removeNthFromEnd = function(head, n) {
// 初始化 dummy 结点
const dummy = new ListNode()
// dummy指向头结点
dummy.next = head
// 初始化快慢指针,均指向dummy
let fast = dummy
let slow = dummy
// 快指针闷头走 n 步
while(n!==0){
fast = fast.next
n--
}
// 快慢指针一起走
while(fast.next){
fast = fast.next
slow = slow.next
}
// 慢指针删除自己的后继结点
slow.next = slow.next.next
// 返回头结点
return dummy.next
}
复杂链表的复制
- 用哈希做映射
新的链表节点用哈希存起来,再通过引用地址找到random指针指向的节点。空间复杂度是O(N)
var copyRandomList = function(head) {
if(!head) return null;
let cur = head,preHead = new Node(),temp = preHead,map = new Map();
while(cur) {
temp.val = cur.val;
temp.next = cur.next ? new Node() : null;
map.set(cur,temp);// 把temp的在值存起来
temp = temp.next;
cur = cur.next;
}
// 初始化,进行第二次遍历
temp = preHead;
// 创建节点之间的关系 先创建链表,再从哈希中取值
while(head) {
// 通过引用地址找到对应的链表节点
temp.random = head.random ? map.get(head.random): null;
head = head.next;
temp = temp.next;
}
return preHead;
};
- 原地复用
通过在原链表中创建新节点,优化了第一种方法的O(N)的空间复杂度
1)创建新节点以及实现新节点和元链表节点的连接
2)根据原链表的rangdom指向去生成新的节点的random的指向
3)链表的分割
var copyRandomList = function(head) {
if (head == null) {
return head;
}
//将拷贝节点放到原节点后面,例如1->2->3这样的链表就变成了这样1->1'->2'->3->3'
for (let node = head, copy = null; node != null; node = node.next.next) {
copy = new Node(node.val);
copy.next = node.next;
node.next = copy;
}
//把拷贝节点的random指针安排上
for (let node = head; node != null; node = node.next.next) {
if (node.random != null) {
node.next.random = node.random.next;
}
}
//分离拷贝节点和原节点,变成1->2->3和1'->2'->3'两个链表,后者就是答案
let newHead = head.next;
for (let node = head, temp = null; node != null && node.next != null;) {
temp = node.next;
node.next = temp.next;
node = temp;
}
return newHead;
};