数组和链表的常见题型
数组在计算机中是以连续空间的形态存在,而链表则是不要求空间连续,每一个元素都保存着下一个元素的地址,使其更加灵活。
约瑟夫环问题
问题描述
著名的约瑟夫问题:编号为 1-N
的 N
个士兵围坐在一起形成一个圆圈,从编号为 1 的士兵开始依次报数(1,2,3… 这样依次报数),数到 m 的 士兵会被淘汰出列,之后的士兵再从 1 开始报数。直到最后剩下一个士兵,求这个士兵的编号。
求解思路
假设我们现在需要使用数组解决该问题,用一个数组存在 1,2,3,4,5 … 编号,要求返回最后一个士兵的编号。思路如下
- 初始化剩下的士兵人数
retainNum
为n
,循环下面的操作,直到全部士兵出圈。 - 初始化
k = 0
,从第一个士兵开始计数,每次遍历到数值不为-1
的元素,则k+1
,如果k = M
,则说明该士兵需要被淘汰出局,则元素的值置为-1
,淘汰的数量num + 1
,且k
重新赋值为 0。 - 遍历数组元素,找到数值不为
-1
的元素,就是最后剩下的士兵。
假设 5 个士兵,数到 3 就淘汰,具体的执行过程如下:
代码实现如下:
/** |
滑动窗口的最大值
问题描述
假设给定一个整形数组 nums
和一个大小为 k
的窗口,k
小于 nums
的长度,窗口从数组的最左边,每次滑动一个数,一直到最右边,返回每次滑动窗口中的最大值的数组。
假设输入的数组为 nums[] = { 3, 5 , -1 , 3 , 2 , 5 , 1 , 6 }
,窗口大小 k = 3。
滑动窗口具体如下,[]
中即滑动窗口的内容
[ 3 5 -1] 3 2 5 1 6 5 |
求解思路
- 遍历数组
nums[]
中的元素num[i]
,执行以下的操作。 - 执行循环:如果队列不为空,且以队列的最后一个元素为下标的数组元素
nums[queue.peekLast()]
小于num[i]
时,将队列的最后一个元素删除。意为:删除队列中较小的元素索引。 - 将当前元素下标 i 添加到队列的尾部。
- 如果队列的队首元素小于
i-k
,则移除队首的元素,说明队首的元素索引已经超过了滑动窗口的长度了,应该抛弃队首的索引。 - 如果
i
大于等于k-1
,那么说明滑动窗口长度已经生效,此时的队列第一个元素作为索引,取出数组中的数值,就是i-k+1
为起始的滑动窗口的最大值。
以 nums[] = {3,5,-1,3,2,5,1,6} 为例,窗口大小 k = 3
。
i = 0
,nums[0] = 3
,队列为空,添加索引 0 到队列尾部,队列为{ 0 }
。
i = 1
,nums[1] = 5
,队列为{ 0 }
,队列尾部元素为 0,nums[0] < nums[1]
,则需要把 0 移除,此时队列为{}
,添加 1 进去,队列变为{ 1 }
。
i = 2
,nums[2] = -1
,队列为{1}
,队列尾部元素为 1,nums[1] > nums[2]
,不移除,直接把索引 2 加到队列中,队列变成{ 1,2 }
,此时i >= k-1
满足,也就是滑动窗口生效。results[i-k+1] = results[0]
的值为以队列的第一个元素 1 为索引的数组元素,即nums[1] = 5
,因此results[0] = 5
。
i = 3
,nums[3] = 3
,此时队列为{ 1,2 }
,队列尾部元素为 2,nums[2] < nums[3]
,因此需要将队列尾部的 2 移除,队列变成{ 1 }
,此时队尾元素为 1,nums[1] = 5 > nums[3]
,不再移除元素。将当前的索引 3 添加至队列中,队列变成{ 1,3 }
。此时的队首元素的为 1,当前遍历索引为 3,符合条件,不做移除,results[i-k+1] = results[1]
的值为以队列的第一个元素 1 为索引的数组元素,即nums[1] = 5
,因此results[1] = 5
。
i = 4
,nums[4] = 2
,当前队列为{ 1, 3 }
,队尾元素为 3,nums[3]
为 3,nums[4] < nums[3]
,因此不需要移除队列尾部的索引元素,直接将当前索引位置 4 加到队列中。队列变成{1,3,4}
,队列的第一个元素(设为first
)为 1,索引为 1 的元素已经不在当前窗口(i-k >= first
),需要移除队列头部元素,因此队列变成{ 3,4 }
,i >= k-1
满足,因此results[i-k+1] = results[4-3+1] = results[2]
的值为以队列的第一个元素 3 (当前队列第一个元素)为索引的数组元素,即nums[3] = 3
,因此results[2] = 3
。
i = 5
,nums[5] = 5
,当前队列为{3,4}
,队列不为空,队尾元素为 4,nums[4] = 2 < nums[5]
,因此需要移除队尾元素 4。队列变成{3}
,队尾元素为 3,nums[3] = 3 < nums[5]
,因此还需要移除队尾元素 3,队列为空{}
。接着将当前索引 5 添加到当前的队列中,队列为{5}
,队首元素是 5,处在当前有效窗口内,i >= k-1
满足,因此results[i-k+1] = results[5-3+1] = results[3]
的值为以队列的第一个元素 5(当前队列第一个元素)为索引的数组元素,即nums[5] = 5
,因此results[3]=5
。
i = 6
,nums[6] = 1
,当前队列为{ 5 }
,队列不为空,队尾元素为 5,nums[5] = 5 > nums[6]
,因此不需要移除队尾元素。直接将当前索引 6 添加到队列中,队列变成为{ 5,6 }
,队首元素为索引 5,当前索引i = 6
,处于有效窗口内,因此不需要移除队首元素。i >= k-1
满足,因此results[i-k+1] = results[6-3+1] = results[4]
的值为以队列的第一个元素 5 (当前队列第一个元素)为索引的数组元素,即nums[5] = 5
,因此results[4] = 5
。
i = 7
,nums[7] = 6
,当前队列为{ 5,6 }
,队列不为空,队尾元素为 6,nums[6] = 1 < nums[7]
,因此需要将队尾元素移除,队列变成{5}
,当前的队尾元素为 5。nums[5] = 5 < nums[7]
,因此还需要将队尾元素移除,此时队列为空{}
,直接将当前的索引 7 添加到队列尾部,队列变成{7}
,队首元素为 7,索引为i = 7
,处于有效的滑动窗口内,因此不需要移除队首元素。i >= k-1
满足,因此results[i-k+1] = results[7-3+1] = results[5]
的值为以队列的第一个元素 7 (当前队列第一个元素)为索引的数组元素,即nums[7] = 6
,因此results[4] = 6
。
i = 8
,超出数组范围,结束。
代码实现
/** |
找链表的倒数第k个元素
问题描述
输入一个链表,输出该链表中倒数第k个节点。
求解思路
首先想到的方法,先用一个指针,从头到尾走完,并且边走边计数,可以获得链表的长度 n
。然后再使用一个指针又从头开始,走到 n-k+1
的位置,就是倒数第 k
个元素。但是这样就需要遍历两次,并不优雅。
因此我们可以让两次遍历一起进行,也就是两个指针,一前一后,前后指针,先让第 1 个指针先走 k 步,然后第 2 个指针开始与第 1 个指针一起走,直到第 1 个指针走到最后的位置,此时第 2 个指针停留的位置就是倒数第 k 个元素。
代码实现
/** |
判断一个链表是否有环
问题描述
给一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。
求解思路
如果我们让一个快指针每次走两步,慢指针每次走一步,如果快慢两个指针能够相遇的话,说明快指针走过环并且已经从后面追上了慢指针,那就可以证明环的存在了。如果没有环,那么快指针会直接走到链表的尾部,到达 null 节点,此时链表肯定不存在环的。
代码实现
/** |
两个链表的第一个公共节点
问题描述
现在有两个链表(没有环),但是存在着共同的部分,找出它们的第一个公共结点,例如,下面的两条链表
求解思路
从图中我们可以得知,其实从相同节点开始,后面那部分链表都是相同的,也就是如果链表可以从后面开始遍历的话,同时移动,就可以获取到相同的节点。但是明显不能从后面开始遍历,那总得想办法从前面开始遍历时,能同时到达共同节点。
于是,第一个链表后面拼接上第二个链表,第二个链表后面拼接上第一个链表,链表等长了,而且相同的那个元素的位置一样,都在倒数第 2 个
第一个和第二个链表都从第一个节点开始比较,只要相等,就说明是第一个公共节点(即是节点 6)
代码实现
const getIntersectionNode = (A, B) => { |
反转链表
问题描述
给一个链表,要求反转链表后,输出新链表的表头节点。
求解思路
只需要使用循环,不断把指向下一个的指针,指向前面的节点。
假设链表是 1 -> 2 -> 3 -> 4
,那么我们需要在开始的时候借助一个 null
节点,当 head
节点不为空的时候,先保存 head
的下一个节点,然后将 head
的 next
指向修改为指向反向,然后移动 head
指针
代码实现
/** |
合并有序链表
问题描述
假设两个单调递增的链表,我们要把它们合成一个链表,合并之后的链表需要保持递增的性质,返回头节点,这该怎么做呢?
求解思路
创建一个 -1
节点的新链表,然后两个链表都从头开始遍历,循环直到一个链表遍历到最后,在这个过程中,哪一个链表的节点小,就加入新的链表后面,移动到后面一个节点,接着比较。
之后遍历两个链表剩下的元素,这些元素肯定比另一个链表的所有元素都大或者相等,直接加入新的链表后面即可。
代码实现
/** |