通常的解题思路
一个屡试不爽的方法:将常用数据结构和常用算法思想都想一遍,看看哪些能解决问题。 常用的数据结构有数组、链表、队、栈、Hash、集合、树、堆。常用的算法思想有查找、排序、双指针、 递归、迭代、分治、贪心、回溯和动态规等等。
1.两个链表第一个公共子节点
剑指offer52题
输入两个链表,找到它们的第一个公共节点。
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数
也是未知的,请设计算法找到两个链表的合并点。
:::info
解题思路:
- 哈希和集合:先遍历A链表,将链表中所有的节点存到一个哈希表或集合中,再遍历第二个链表B,找到第一次出现再哈希表或集合中的节点,即第一个公共节点,如果没有,就说明不存在
- 栈:将A,B链表中的节点分别压入两个栈中,再同时出栈,如果出栈第一个元素不相同,就说明不存在公共子节点,如果相同,则依次出栈找到不同的节点。 :::
方法一:哈希和集合
public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
方法1:哈希和集合
Set<ListNode> set=new HashSet<>();
while (headA!=null){
set.add(headA);
headA=headA.next;
}
while (headB!=null){
if(set.contains(headB)){
return headB;
}
headB=headB.next;
}
return null;
}
方法二:栈
public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
//方法2:栈
Stack<ListNode> stackA=new Stack<>();
Stack<ListNode> stackB=new Stack<>();
while (headA!=null){
stackA.add(headA);
headA=headA.next;
}
while (headB!=null){
stackB.add(headB);
headB=headB.next;
}
ListNode preNode=null;
while (stackA.size()>0&&stackB.size()>0){
if(stackA.peek()==stackB.peek()){
preNode=stackA.pop();
stackB.pop();
}else {
break;
}
}
return preNode;
}
2.判断链表是否为回文链表
234.回文链表 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1] 输出:true
示例 2:
输入:head = [1,2] 输出:false
:::info
解题思路:
- 数组:将链表的元素都存到一个数组中,再从数组两端向中间遍历
- 栈:链表节点依次入栈,再出栈的同时,让链表从头开始遍历,比较每一个元素
- 优化:将链表的元素压入栈中的同时,计算链表的长度,再比较的时候,只要比较一半的元素
- 反转链表:创建一个newList,将会问链表反转存入newList中,再比较前一半的元素
- 快慢指针: :::
方法一:栈
class Solution {
public boolean isPalindrome(ListNode head) {
//栈
Stack<ListNode> stack=new Stack<>();
ListNode cur=head;
int len=0;
while (cur!=null){
stack.push(cur);
cur=cur.next;
len++;
}
int count=len/2;
while (count>0){
if(head.val!=stack.pop().val){
return false;
}else {
count--;
head=head.next;
}
}
return true;
}
}
方法二:快慢指针
class Solution {
public boolean isPalindrome(ListNode head) {
//快慢指针
ListNode slow=head;
ListNode fast=head;
Stack<ListNode> stack=new Stack<>();
int count=0;
while (fast!=null){
stack.push(slow);
fast=fast.next;
slow=slow.next;
count++;
if(fast!=null){
fast=fast.next;
count++;
}else {
break;
}
}
if(count%2==1){
stack.pop();
}
while (slow!=null){
if(slow.val!=stack.pop().val){
return false;
}
slow=slow.next;
}
return true;
}
}
3.合并有序列表
3.1合并两个有序列表
合并两个有序列表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = [] 输出:[]
示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
:::info
解题思路:
同时遍历两个链表,定义一个newHead,每次将两个链表中较小的节点存入到newHead下一个节点
:::
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newNode=new ListNode(-1);
ListNode res=newNode;
ListNode cur1=list1;
ListNode cur2=list2;
while (cur1!=null&&cur2!=null){
if(cur1.val>cur2.val){
newNode.next=new ListNode(cur2.val);
cur2=cur2.next;
}else {
newNode.next=new ListNode(cur1.val);
cur1=cur1.next;
}
newNode=newNode.next;
}
if (cur1!=null){
newNode.next=cur1;
}
if (cur2!=null){
newNode.next=cur2;
}
return res.next;
}
}
3.2合并K个链表
合并K个链表 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1: 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6 示例 2: 输入:lists = [] 输出:[] 示例 3: 输入:lists = [[]] 输出:[] :::info 解题思路:
- 合并k个链表,有多种方式,例如堆、归并等等。如果面试遇到,我倾向先将前两个合并,之后再将后面的逐步合并进来,这样的的好处是只要将两个合并的写清楚,合并K个就容易很多,现场写最稳妥:
- 可以将k个链表中的元素全部存入到集合中,集合排序后,在添加到链表中 :::
方法一
public ListNode mergeKLists(ListNode[]lists){
ListNode res=null;
for (ListNode list:lists){
res mergeTwoLists(res,list);
return res;
}
}
3.3一道无聊的好题
合并两个链表
给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。
请你将 list1 中下标从 a 到 b 的全部节点都删除,并将list2 接在被删除节点的位置。
下图中蓝色边和节点展示了操作后的结果:
请你返回结果链表的头指针。
示例 1:
输入:list1 = [0,1,2,3,4,5], a = 3, b = 4, list2 = [1000000,1000001,1000002] 输出:[0,1,2,1000000,1000001,1000002,5] 解释:我们删除 list1 中下标为 3 和 4 的两个节点,并将 list2 接在该位置。上图中蓝色的边和节点为答案链表。
示例 2:
输入:list1 = [0,1,2,3,4,5,6], a = 2, b = 5, list2 = [1000000,1000001,1000002,1000003,1000004] 输出:[0,1,1000000,1000001,1000002,1000003,1000004,6] 解释:上图中蓝色的边和节点为答案链表。
:::info
解题思路:
找到要删除的前一个节点pre,和删除后的下一个节点behind
然后令pre指向list2,list2的最后一个节点指向behind
:::
class Solution {
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
ListNode cur =list1;
//移除部分的前一个节点
ListNode pre=null;
//移除部分后一个节点
ListNode behind=null;
while (b>=0){
if(a-1==0){
pre=cur;
}
cur=cur.next;
b--;
a--;
}
behind=cur;
pre.next=list2;
ListNode cur2=list2;
while (cur2.next!=null){
cur2=cur2.next;
}
cur2.next=behind;
return list1;
}
}
4.双指针专题
4.1寻找中间节点
寻找中间节点 给你单链表的头结点 head ,请你找出并返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。
示例 2:
输入:head = [1,2,3,4,5,6] 输出:[4,5,6] 解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。
:::info 解题思路: 双指针:
- 定义两个指针 slow和fast,
- fast每次走两步,slow每次走一步
- 如果节点个数为奇数,fast走到尾节点时,slow正好位于中间节点
- 如果节点个数为偶数,fast走到null(尾节点的下一个节点)slow正好为第二个中间节点
- 当fast为空或fast下一个节点为空的时候跳出循环
注意:搞清楚终止遍历的条件,
当fast为空或fast下一个节点为空的时候跳出循环
:::
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while (fast!=null&&fast.next!=null){
fast=fast.next;
slow=slow.next;
if(fast!=null){
fast=fast.next;
}
}
return slow;
}
}
4.2寻找倒数第k个元素
寻找倒数第k个元素 实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。 **注意:**本题相对原题稍作改动 示例: 输入: 1->2->3->4->5 和 k = 2 输出: 4 说明: 给定的 k 保证是有效的。
:::info 解题思路: 同样也是双指针问题:因为要找倒数第k个元素 我们定义两个指针fast和slow,让fast先走k步,再同时让两指针往后移动,直到fast节点为空 放回slow指针即可 :::
class Solution {
public int kthToLast(ListNode head, int k) {
ListNode fast=head;
ListNode slow=head;
while (k>0){
fast=fast.next;
k--;
}
while (fast!=null){
fast=fast.next;
slow=slow.next;
}
return slow.val;
}
}
4.3旋转链表
旋转链表 你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k_ _个位置。
示例 1:
输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3]
示例 2:
输入:head = [0,1,2], k = 4 输出:[2,0,1]
:::info 解题思路:
- 统计节点个数count
- 同时将每个节点存入一个List集合中
- 如果k%count==0,直接返回头结点
- 将否则,尾节点指向头结点
- 计算index=k%count
- 让集合中第count-index-1个节点指向空
- 返回第count-index节点
注意:具体让第几个节点指向空,返回第几个节点,最笨的方法就是将具体数值带入求取 :::
class Solution {
public ListNode rotateRight(ListNode head, int k) {
ListNode cur=head;
List<ListNode> list=new ArrayList<>();
if(cur==null){
return null;
}
if(cur.next==null){
return cur;
}
int count=1;
while (cur.next!=null){
list.add(cur);
count++;
cur=cur.next;
}
list.add(cur);
int index = k % count;
if(index==0){
return head;
}
cur.next=head;
ListNode listNode = list.get(count-index-1);
listNode.next=null;
return list.get(count-index);
}
}
5.删除链表元素专题
5.1删除特定节点
移除链表元素 给你一个链表的头节点 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 输出:[]
:::info 解题思路:
- 定义一个虚拟节点,让它指向链表头节点
- 依次遍历它的下一个节点,如果等于val
- 就cur=cur.next; :::
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode cur=new ListNode(-1);
ListNode res=cur;
cur.next=head;
if(cur==null) return null;
while (cur.next!=null){
if(cur.next.val==val){
cur.next=cur.next.next;
}else {
cur=cur.next;
}
}
return res.next;
}
}
5.2删除倒数第n个节点
删除倒数第n个节点 给你一个链表,删除链表的倒数第 n_ _个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1 输出:[]
示例 3:
输入:head = [1,2], n = 1 输出:[1]
:::info 解题思路:
- 利用双指针 fast ,slow ,fast比slow先走n步
- 当fast指针到末尾时候
- 删除slow下一个节点,即slow.next=slow.next.next;
- 返回链表头结点
注意:如果要删除第一个节点(特殊情况),即第一步fast==null 时候,我们直接返回head.next :::
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode fast=head;
ListNode slow=head;
//利用双指针 fast ,slow ,fast比slow先走n步
while (n>0){
if(fast==null){
return null;
}
fast=fast.next;
n--;
}
//如果要删除第一个节点(特殊情况),即第一步fast==null 时候,我们直接返回head.next
if(fast==null){
return head.next;
}
//当fast指针到末尾时候
while (fast!=null){
if(fast.next==null){
break;
}
fast=fast.next;
slow=slow.next;
}
//删除slow下一个节点,即slow.next=slow.next.next;
slow.next=slow.next.next;
//返回链表头结点
return head;
}
}
5.3删除重复元素
5.3.1重复元素保留一个
重复元素保留一个 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例 1:
输入:head = [1,1,2] 输出:[1,2]
示例 2:
输入:head = [1,1,2,3,3] 输出:[1,2,3]
:::info 解题思路:
- 双指针遍历, :::
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode res=new ListNode(-101);
ListNode newHead=res;
ListNode cur=head;
while (cur!=null){
if(newHead.val!=cur.val){
newHead.next=new ListNode(cur.val);
newHead=newHead.next;
}
cur=cur.next;
}
return res.next;
}
}
5.3.2 重复元素都不要
重复元素都不要 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例 1:
输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5]
示例 2:
输入:head = [1,1,1,2,3] 输出:[2,3]
class Solution {
public ListNode deleteDuplicates(ListNode head) {
HashMap<Integer,Integer> hm=new HashMap<>();
ListNode cur=head;
while (cur!=null){
hm.put(cur.val,hm.getOrDefault(cur.val,0)+1);
cur=cur.next;
}
ListNode res=new ListNode();
ListNode myres=res;
Set<Map.Entry<Integer, Integer>> entries = hm.entrySet();
List<Integer> list=new ArrayList<>();
for (Map.Entry<Integer, Integer> entry : entries) {
if(entry.getValue()==1){
list.add(entry.getKey());
}
}
list.sort(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
for (Integer integer : list) {
res.next=new ListNode(integer);
res=res.next;
}
return myres.next;
}
}
:::info 参考的解题思路: 直接比较cur.next和cur.next.next, 如果cur.next!.val=cur.next.next.val,让cur=cur.next 如果相同的化,就找到下一个不等于cur.next.val的节点,并且跳过中间所有的节点,重新判断cur.next!.val=cur.next.next.val,让cur=cur.next :::
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode res = new ListNode(0, head);
ListNode cur = res;
while (cur.next != null && cur.next.next != null) {
if (cur.next.val == cur.next.next.val) {
int x = cur.next.val;
while (cur.next != null && cur.next.val == x) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return res.next;
}
}
6.再论第一个公共子节点
在我们第一题寻找公共子节点部分,我们看到使用Hsh或者栈是可以解决问题的,但是这样可能只能得80 分,因为这不一定是面试官想要的答案,为什么呢?因为不管你使用栈还是集合都需要开辟一个O()的空 间,那如果只使用一两个变量能否解决问题呢? 可以的,这里我们就看一下另外两种解决方式。
6.1拼接两个字符串
:::info
解题思路:
先看下面的链表A和B:
A:0-1-2-3-4-5
B:a-b-4-5
如果分别拼接成AB和BA会怎么样呢?
AB:0-1-2-3-4-5-a-b-4-5
BA:a-b-4-5-0-1-2-3-4-5
我们发现拼接后从最后的4开始,两个链表是一样的了,自然4就是要找的节点,所以可以通过拼接的方式
来寻找交点。这么做的道理是什么呢?我们可以从几何的角度来分析。我们假定A和B有相交的位置,以交
点为中心,可以将两个链表分别分为left a和right a,left b和right b这样四个部分,并且right a和
right b是一样的,这时候我们拼接AB和BA就是这样的结构:
我们说right a和right b是一样的,那这时候分别遍历AB和BA是不是从某个位置开始恰好就找到了相交的
点了?
这里还可以进一步优化,如果建立新的链表太浪费空间了,我们只要在每个链表访问完了之后,调整到一
下链表的表头继续遍历就行了,于是代码就出来了:
这里很多人会对为什么循环体里f(p1!=p2)这个判断有什么作用,我们在代码后面解释
:::
public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
//方法3:拼接两个字符串
if(headA==null||headB==null){
return null;
}
ListNode p1=headA;
ListNode p2=headB;
while (p1!=p2){
p1=p1.next;
p2=p2.next;
if(p1!=p2){
//如果一个链表访问完,让他指向另一个链表
if(p1==null){
p1=headB;
}
if(p2==null){
p2=headA;
}
}
}
return p1;
}
6.2差和双指针
:::info 解题思路:
- 分别求出两个链表的长度len1,len2
- 求出差值:n
- 让长的链表先走n步
- 然后再同时遍历两个链表 :::
public class Solution {
public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){
//方法4:差和双指针
ListNode cura=headA;
int lena=0;
while (cura!=null){
lena++;
cura=cura.next;
}
ListNode curb=headB;
int lenb=0;
while (curb!=null){
lenb++;
curb=curb.next;
}
cura=headA;
curb=headB;
if(lenb>lena){
int sub=lenb-lena;
while (sub>0){
curb=curb.next;
sub--;
}
}
if(lena>lenb){
int sub=lena-lenb;
while (sub>0){
cura=cura.next;
sub--;
}
}
while (cura!=null&&curb!=null){
if(cura.val==curb.val){
return cura;
}
cura=cura.next;
curb=curb.next;
}
return null;
}