于2021.12.01开始记录。剑指offer在牛客网或者力扣上面都可以刷的,我选择了牛客网,下面的题目顺序也是按照牛客网上面的顺序来的。
JZ6 从尾到头打印链表
题目描述:输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
1.1 解法1:利用栈
思路:利用栈先进后出的特点,首先遍历链表,将链表中的结点压入堆栈;然后将结点弹出,放入到结果list中。
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
Stack<ListNode> stack = new Stack<>();
ListNode cur = listNode;
while(cur != null){
stack.push(cur);
cur = cur.next;
}
ArrayList<Integer> res = new ArrayList<>();
while(!stack.empty()){
res.add(stack.pop().val);
}
return res;
}
}
1.2 解法2:反转链表后输出
思路:首先利用反转链表的思想,将原链表进行反转,然后在遍历反转以后的链表,进行输出。
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ListNode cur = listNode;
ListNode pre = null;
while(cur != null){
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
ArrayList<Integer> res = new ArrayList<>();
while(pre != null){
res.add(pre.val);
pre = pre.next;
}
return res;
}
}
JZ24 反转链表
题目描述:给定一个单链表的头结点pHead,长度为n,反转该链表后,返回新链表的表头。
1.1 解法1:双指针迭代法
思路:
首先申请两个指针,第一个指针为pre,最初是指向null的,第二个指针指向head,然后让cur去遍历原链表。
每遍历一次,先用辅助变量tmp将cur的下一个节点保存下来,然后cur的next指向pre,再将pre和cur前进一步。当cur为null时,说明遍历完了,此时pre为原链表中的最后一个结点。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;//创建一个结点pre指向null
ListNode cur = head;
while(cur != null){
ListNode tmp = cur.next;//创建辅助结点,用于保存当前结点的下一个结点
cur.next = pre;//当前结点指向cur
//pre和cur结点都前进一步
pre = cur;
cur = tmp;
}
return pre;
}
}
1.2 解法2:递归法
递归的方法不太好理解,借鉴了力扣大佬的讲解:递归法力扣讲解,这个链接中幻灯片部分解释的很好,可以看看。
递归的理解难点:返回值cur为什么保存不变?
将cur理解为返回的是已经反转过的部分的头指针,而这个头指针是确定的。每次递归,cur并没有改变,改变的是head。
递归的作用就是为了找到链表的最后一个结点,然后head一直改变,从而改变了指针的方向。
下面我放了一张图片,演示了程序的运行流程。
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode cur = ReverseList(head.next);
head.next.next = head;
head.next = null;
return cur;
}
}
不得不说,递归实现的代码还是很简单的,就是一开始不理解递归,后面还是要多练习的。
JZ25 合并两个排序的链表
题目描述:输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
1.1 解法1:迭代法
思路:
①首先定义一个虚拟头结点,用于表示合并后的新链表。然后定义一个cur,去指向这个合并的新链表。
②当l1和l2都不为空时,比较哪一个链表的结点值更小,将较小值结点添加到结果链表中,然后对应链表中的结点后移一位,同时结果链表指针也要后移一位。
③最后肯定是有一个链表先遍历完成,此时另一个链表还没有挂在结果链表上,因此需要进行判断。当循环终止时,假如l1先为null,此时l2中剩余的部分元素还未添加到结果链表中,因此需要进行添加;假如l2先为null,此时l1中剩余的部分元素还未添加到结果链表中,因此需要进行添加。
此种方法为了记忆,我简称为三指针拉链法:其中链表1对应一个指针l1,链表2对应一个指针l2,结果链表对应一个指针cur,整个过程类似于拉拉链,将两个链表都要穿起来。
总结:整个方法在LeetCode88合并两个有序数组以及归并排序中都有体现,相同的思路。
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
ListNode dummyHead = new ListNode(-1);
ListNode cur = dummyHead;
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
cur.next = list1;
list1 = list1.next;
}
else{
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
cur.next = (list1 == null ? list2 : list1);
return dummyHead.next;
}
}
1.2 解法2:递归法
参考链接:递归法实现合并,看下面的幻灯片讲解部分。
思路:
①递归的终止条件:当链表l1为空或者l2为空时,结束
②判断l1和l2哪个更小,然后让较小结点的next指向其余结点的合并结果(调用递归)。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
if(list1.val <= list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}
else{
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}
}
}
JZ52 两个链表的第一个公共结点
题目描述:输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。
1.1 解法1:哈希表
本题要找两个链表的公共结点,其实就是在链表2中找链表1中曾经出现过的结点。由于哈希表结构常常用来判断一个元素是否出现过,因此本题考虑使用哈希表解决。
思路:
①首先遍历链表pHead1,将链表中的每个结点添加到哈希表中。这里只需要存储链表结点即可,因此使用HashSet即可。这里要注意存储的是对象,而不是数值,虽然有时里面的数值相同但是对象是不一样的,比如4-1-8-4-5的情况,当存储时第一个4和第二个4都会存储进去,因为它们的地址不相同,不是同一个对象,只是里面的数值是相同的。
②然后遍历链表pHead2,对于遍历到的每一个结点,都判断该结点是否出现在哈希表中:
如果当前结点不在哈希表中,则继续遍历下一个结点;
如果当前结点在哈希表中,那么后面的所有节点都在哈希表中,因此这个结点就是两个链表相交的结点,返回即可。
import java.util.HashSet;
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
HashSet<ListNode> nodes = new HashSet<>();
while(pHead1 != null){
nodes.add(pHead1);
pHead1 = pHead1.next;
}
while(pHead2 != null){
if(nodes.contains(pHead2)){
return pHead2;
}
else{
pHead2 = pHead2.next;
}
}
return null;
}
}
1.2 解法2:转换为环形链表找入口问题思路:
思路:
将其中一个链表首尾相接,构成环,问题转换为判断另一个链表是否有环路,如果有环路找到环路入口的问题。
第一个链表首尾相接时,要记下相接的位置,最后记着拆掉。
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1 == null || pHead2 == null){
return null;
}
ListNode last = pHead2;
while(last.next != null){
last = last.next;
}
last.next = pHead2;
ListNode slow = pHead1;
ListNode fast = pHead1;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
ListNode slow1 = slow;
ListNode fast1 = pHead1;
while(slow1 != fast1){
slow1 = slow1.next;
fast1 = fast1.next;
}
last.next = null;
return slow1;
}
}
last.next = null;
return null;
}
}
注意:此种解法首先要进行特殊情况判断,否则会出现空指针异常。针对上面程序,假如链表2为空,从而last.next = pHead2;
就报错,因为last本来就是空的,根本就没有next。
1.3 解法3:双指针,互走对方的路
在力扣下面看到有意思的评论:走到尽头见不到你,于是走过你来时的路,等到相遇时才发现,你也走过我来时的路。
思路:
首先进行特殊情况判断:当链表pHead1或pHead2为空时,一定不相交,直接返回null
当pHead1和pHead2都不为空时,让pA指向pHead1,pB指向pHead2。如果pA不为空,那么pA移到下一个结点;如果为空,让pA指向headB的头结点。如果pB不为空,那么pB移到下一个结点;如果为空,让pB指向headA的头结点。当pA和pB指向同一个结点时,此时找到交点。
为什么这种方法可行呢?下面图片针对链表相交以及不相交的情况都给出了证明。
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1 == null || pHead2 == null){
return null;
}
ListNode pA = pHead1;
ListNode pB = pHead2;
while(pA != pB){
pA = (pA == null ? pHead2 : pA.next);
pB = (pB == null ? pHead1 : pB.next);
}
return pA;
}
}
JZ23 链表中环的入口结点
题目描述:给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null
1.1 解法1:哈希表
思路:遍历链表中的所有节点,如果当前节点已经存在于哈希表中,则说明该链表是环形链表;否则就将该节点放入哈希表中,继续遍历下一个节点,重复此过程。
import java.util.HashSet;
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
HashSet<ListNode> nodes = new HashSet<>();
ListNode cur = pHead;
while(cur != null){
if(nodes.contains(cur)){
return cur;
}
else{
nodes.add(cur);
cur = cur.next;
}
}
return null;
}
}
1.2 解法2:快慢指针
①判断链表是否有环:链表找环路问题的通用解法就是用快慢指针,即Floyed判圈法(龟兔赛跑算法)。给定快慢指针fast和slow,起始位置都在链表的开头。每次快指针fast前进2步,慢指针slow前进1步。如果fast指针可以走到头,那说明链表无环;如果存在环路,那么两个指针一定会相遇。
②如何寻找环的入口:当slow和fast第一次相遇时,将fast重新移动到链表开头,然后让slow和fast每次都前进一步,当slow和fast第二次相遇时,相遇的节点即为环路的开始节点。
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
if(pHead == null){
return null;
}
ListNode slow = pHead;
ListNode fast = pHead;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
ListNode slow1 = slow;
ListNode fast1 = pHead;
while(slow1 != fast1){
slow1 = slow1.next;
fast1 = fast1.next;
}
return slow1;
}
}
return null;
}
}
JZ22 链表中倒数最后k个结点
题目描述:输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。将倒数问题转换为正数问题。
1.1 解法1:两次遍历
思路:第一轮遍历,确定链表的长度;第二轮遍历,找到这个倒数第k个结点,然后进行输出。
注意:题目中说明如果该链表长度小于k,请返回一个长度为 0 的链表,因此这个特殊情况,要单独判断下
这行代码不能缺少:if(length < k) return null; //特殊情况判断
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode FindKthToTail (ListNode pHead, int k) {
// write code here
int length = getLength(pHead);
if(length < k) return null;
ListNode cur = pHead;
for(int i = 0;i < length - k;i++){
cur = cur.next;
}
return cur;
}
public int getLength(ListNode head){
int length = 0;
while(head != null){
length++;
head = head.next;
}
return length;
}
}
这种解法能实现,但是好像不太符合题目要求的时间复杂度为 O ( n ) O(n) O(n),因为两次遍历其时间复杂度为 O ( n 2 ) O(n^2) O(n2)
1.2 解法2:先后指针
思路:定义先后两个指针slow和fast,让fast先走k步,然后slow和fast同时走,当fast为null时,slow刚好走到倒数第k个结点,然后输出此时的slow。
特殊情况判断:fast先走k步时,边走边判断。假如fast为空了,直接返回null,此时就说明k大于链表的长度。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode FindKthToTail (ListNode pHead, int k) {
// write code here
ListNode slow = pHead;
ListNode fast = pHead;
for(int i = 0;i < k;i++){
if(fast == null) return null;
fast = fast.next;
}
while(fast != null){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
1.3 解法3:栈
思路:利用栈先进后出的特点,可以方便的提取出后面倒数的元素。因此后面的结点会放在栈的栈顶,从而可以方便的弹出。
首先从头到尾遍历链表,将结点放入到栈中。然后进行特殊情况判断,如果栈的大小比k要小,直接return null即可。然后从栈顶弹出k个元素,最后一个出栈的元素就是结果。
import java.util.Stack;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode FindKthToTail (ListNode pHead, int k) {
// write code here
Stack<ListNode> stack = new Stack<>();
ListNode cur = pHead;
while(cur != null){
stack.push(cur);
cur = cur.next;
}
if(stack.size() < k) return null;
ListNode res = null;
for(int i = 0;i < k;i++){
res = stack.pop();
}
return res;
}
}
JZ18 删除链表的节点
题目描述:给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
1.1 解法1
同leetcode203.移除链表元素的题目。
分析两个题目,有不同点:203题中没有指明链表中节点值互不相同(意味着可能要删除的数值对应着多个节点),因此while循环一直判断,直到cur.next=null;而本题中,题目说明了保证链表中节点的值互不相同,因此一旦找到了等于删除数值的节点,就不用在执行while循环了,直接break即可。
思路:
需要创建一个虚拟头结点,然后定义一个临时结点cur,初始时指向虚拟头结点。接着让cur去遍历链表,注意循环的条件为cur.next不为空。
循环中,如果当前结点的下一个结点对应的数值等于val,那么就要删除掉当前结点的下一个结点,于是让cur.next=cur.next.next;如果不等于,此时就让cur往前移动一位,即cur=cur.next。
由于题目中保证链表中节点的值互不相同,因此可以找到了待删除结点,就直接break。
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @param val int整型
* @return ListNode类
*/
public ListNode deleteNode (ListNode head, int val) {
// write code here
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
ListNode cur = dummyHead;
while(cur.next != null){
if(cur.next.val == val){
cur.next = cur.next.next;
break; //比203题,多的一行判断
}
else{
cur = cur.next;
}
}
return dummyHead.next;
}
}
JZ35 复杂链表的复制
题目描述:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。 下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。
1.1 解法1:哈希表
import java.util.HashMap;
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
if(pHead == null){
return null;
}
HashMap<RandomListNode,RandomListNode> map = new HashMap<>();
RandomListNode cur = pHead;
while(cur != null){
map.put(cur,new RandomListNode(cur.label));
cur = cur.next;
}
cur = pHead;
while(cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(pHead);
}
}
1.2 解法2:拼接+拆分(原地法)
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
if(pHead == null){
return null;
}
RandomListNode cur = pHead;
// 复制各节点,并构建拼接链表
while(cur != null){
RandomListNode tmp = new RandomListNode(cur.label);
tmp.next = cur.next;
cur.next = tmp;
cur = tmp.next;
}
// 构建新节点的random指向
cur = pHead;
while(cur != null){
if(cur.random != null){
cur.next.random = cur.random.next;
}
cur = cur.next.next;
}
// 拆分两个链表
RandomListNode pre = pHead; //pre指向原链表头结点
cur = pHead.next; //cur指向新链表头结点
RandomListNode pNewHead = pHead.next; //pNewHead为新链表头结点
while(cur.next != null){
pre.next = pre.next.next;
cur.next = cur.next.next;
pre = pre.next;
cur = cur.next;
}
pre.next = null; //处理原链表末尾节点
return pNewHead;
}
}
JZ76 删除链表中重复的结点
题目描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5
1.1 解法1:哈希表+创建新链表
思路:本质就是创建一个新链表,将不重复的元素挂在新链表的后面。
①第一次遍历链表,统计链表中元素出现的次数,将链表中元素及其出现的次数放入哈希表中。
②然后创建一个新的虚拟头结点,作为结果链表的头结点,其实相当于把这个结果链表给创建出来。
③第二次遍历链表时,用一个指针cur去遍历原链表,用一个指针pre去指向新链表。如果链表中该元素出现的次数为1,那么将该元素添加到结果链表的后面。另外,无论这个次数是否等于1,cur指针都要向前移动,因此这个语句不是在if对应的else中,而是单独的列出来。
import java.util.HashMap;
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
HashMap<Integer,Integer> map = new HashMap<>();
ListNode cur = pHead;
while(cur != null){
map.put(cur.val,map.getOrDefault(cur.val,0) + 1);
cur = cur.next;
}
cur = pHead;
ListNode dummyHead = new ListNode(-1);
ListNode pre = dummyHead;
while(cur != null){
if(map.get(cur.val) == 1){
pre.next = new ListNode(cur.val);
pre = pre.next;
}
cur = cur.next;
}
return dummyHead.next;
}
}
1.2 解法2:双指针原地删除
这个解法并没有创建新的链表,而是通过双指针,直接在原链表上面进行操作,是一种原地删除的方法。
思路:
①定义一个 dummy 头结点,链接上原链表,cur 指向原链表头部,pre指向dummy头结点
②当前结点value != 当前结点的下一结点value。那么让pre指针和当前指针都前进一步
③当前结点value == 当前结点的下一结点value。那么就让 cur 一直往下走直到 当前结点value != 当前结点的下一结点value,然后此时的cur指向了重复元素的最后一个元素,由于后面还有可能出现其他数值是重复的元素,因此不能动pre指针,所以让 pre->next = cur->next,然后当前结点移至下一结点。
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
ListNode dummyHead = new ListNode(-1);
dummyHead.next = pHead;
ListNode cur = pHead;
ListNode pre = dummyHead;
while(cur != null && cur.next != null){
if(cur.next.val == cur.val){
while(cur.next != null && cur.next.val == cur.val){
cur = cur.next;
}
pre.next = cur.next;
cur = cur.next;
}
else{
pre = cur;
cur = cur.next;
}
}
return dummyHead.next;
}
}