赞
踩
大家好,我是小曾哥,今天这篇内容作为牛客101道必刷题的开篇,是我亲自刷题的一些方法,以及一些心得,高效刷题,告别题海战术,举一反三。
现在分享给大家,后续还会继续更新牛客系列,小曾哥带大家一起刷牛客编程题!
1、给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
如当输入链表{1,2,3}时, 经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。 以上转换过程如下图所示:
输入:{1,2,3}
返回值:{3,2,1}
输入:{}
返回值:{}
说明:空链表则输出空
常规思路:迭代
具体细节:在遍历链表时,将当前节点的next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。
拆解步骤:
1、因为链表结尾是 null,所以让 pre 的值是 null, p 就表示我们的头部
2、因为 p 的 next 成员马上就要指向 pre, 如果不保存 p 的下一个节点就会使其丢失,所以通过临时变量 t 保存它
3、让 P 的 next 成员指向 pre
4、pre 移动到 p 的位置,p 移动到 t 的位置,此时我们就回到了第一步中的情况
5、保持这个循环不变式直到 p 移动到原链表结尾我们就获得了翻转之后的链表。
/* public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } }*/ public class Solution { public ListNode ReverseList(ListNode head) { //pre指针:用来指向反转后的节点,初始化为null ListNode pre = null; //当前节点指针 ListNode cur = head; //循环迭代 while(cur!=null){ //Cur_next 节点,永远指向当前节点cur的下一个节点 ListNode Cur_next = cur.next; //反转的关键:当前的节点指向其前一个节点(注意这不是双向链表,没有前驱指针) cur.next = pre; //更新pre pre = cur; //更新当前节点指针 cur = Cur_next ; } //为什么返回pre?因为pre是反转之后的头节点 return pre; } }
将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)。
输入:{1,2,3,4,5},2,4
返回值:{1,4,3,2,5}
输入:{5},1,1
返回值:{5}
思路步骤:
1、固定子区间外的节点
2、在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。
curr:指向待反转区域的第一个节点 left;
Cur_next:永远指向 curr 的下一个节点,循环过程中,curr 变化以后 Cur_next 会变化;
pre:永远指向待反转区域的第一个节点 left 的前一个节点,在循环过程中不变。
public class Solution { /** * * cur:指向待反转区域的第一个节点 left; * Cur_next:永远指向 cur 的下一个节点,循环过程中,cur 变化以后 Cur_next 会变化; * pre:永远指向待反转区域的第一个节点 left 的前一个节点,在循环过程中不变。 */ public ListNode reverseBetween (ListNode head, int m, int n) { //设置虚拟头节点 ListNode dummyNode = new ListNode(-1); dummyNode.next =head; //将pre指向头结点 ListNode pre = dummyNode; // 循环将pre指向m的前一个位置 for(int i=0;i<m-1;i++){ pre = pre.next; } // 设置cur指向索引m的结点 ListNode cur = pre.next; ListNode Cur_next ; for(int i=0;i<n-m;i++){ // 将cur_next指向cur的下一个 Cur_next = cur.next; // 进行第一步操作,将cur下一个结点指向cur_next的下一个位置 cur.next = Cur_next.next; // 进行第二步操作,将cur_next下一步指向pre.next当前位置 Cur_next .next = pre.next; // 进行第三步操作,将pre.next指向cur_next,以此类推 pre.next = Cur_next ; // 注意:1、2、3步骤不能调换,否则中间变量会失效。 } return dummyNode.next; } }
描述:将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表 如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样你不能更改节点中的值,只能更改节点本身。
示例
输入:{1,2,3,4,5},2
返回值:{2,1,4,3,5}
题目分析:跟题目1类相似,区别在于取出k个节点进行翻转,因此首先是找到k个结点的集合,然后对每个结点集合进行翻转操作。
具体步骤:
1、找到待翻转的k个节点(注意:若剩余数量小于 k 的话,则不需要反转,因此直接返回待翻转部分的头结点即可)。
2、对其进行翻转。并返回翻转后的头结点(注意:翻转为左闭右开区间,所以本作的尾结点其实就是下一作的头结点)。
3、对下一轮 k 个节点也进行翻转操作。
4、将上一轮翻转后的尾结点指向下一轮翻转后的头节点,即将每一轮翻转的k的节点连接起来。
方法一 public class Solution { /** * * @param head ListNode类 * @param k int整型 * @return ListNode类 */ public ListNode reverseKGroup (ListNode head, int k) { // write code here ListNode cur = head; int count = 0; //找到k个节点集合 while(cur !=null && count !=k){ cur = cur.next; count ++; } if(count == k){ cur = reverseKGroup(cur,k); // 反转列表 while(count !=0){ count --; ListNode tmp = head.next; // 将头节点连接下一个翻转的节点 head.next = cur; cur = head; head = tmp; } // 拼接后续的链表 head = cur; } return head; } } 方法二:充分利用题目1的代码,更方便理解 public ListNode reverseKGroup (ListNode head, int k) { // write code here if(head == null || head.next == null) return head; ListNode tail = head; for(int i =0;i<k;i++){ if(tail == null) return head; tail = tail.next; } // 开始反转 ListNode newHead = reverse(head, tail); //下次开始的位置就是tail head.next = reverseKGroup(tail,k); return newHead; } private ListNode reverse(ListNode head, ListNode tail) { ListNode pre = null; ListNode next = null; while (head != tail) { next = head.next; head.next = pre; pre = head; head = next; } return pre; }
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
此题leetcode上也有一样的题目:21
主要介绍几种方法,方便理解
函数功能:合并两个单链表,返回两个单链表头结点值小的那个节点。
如果知道了这个函数功能,那么接下来需要考虑2个问题:
递归函数结束的条件是什么?
递归函数一定是缩小递归区间的,那么下一步的递归区间是什么?
对于问题1.对于链表就是,如果为空,返回什么
对于问题2,跟迭代方法中的一样,如果list1的所指节点值小于等于list2所指的结点值,那么list1后续节点和list2节点继续递归
递归版本很好理解 public class Solution { public ListNode Merge(ListNode list1,ListNode list2) { if(list1 == null) return list2; if(list2 == null) return list1; if(list1.val <= list2.val){ list1.next = Merge(list1.next,list2); return list1; }else{ list2.next = Merge(list2.next,list1); return list2; } }
其实用迭代方法,也是常规考虑方法
步骤:
1、首选新创建一个链表head
2、如果list1指向的结点值小于等于list2指向的结点值,则将list1指向的结点值链接到head的next指针,然后head指向list1,list1继续指向下一个结点值;
3、否则,则将list2指向的结点值链接到head的next指针,然后head指向list2,让list2指向下一个结点值;继续循环步骤2,3
4、直到list1或者list2为空,将list1或者list2剩下的部分链接到head的后面
public ListNode Merge(ListNode list1,ListNode list2) { // 单链表完成 ListNode head = new ListNode(-1); head.next = null; ListNode root = head; while(list1 !=null && list2 !=null){ if(list1.val <= list2.val){ head.next = list1; head = list1; list1 = list1.next; }else{ head.next = list2; head = list2; list2 = list2.next; } } // 把剩余的链表直接连接到合并后的为尾部 if(list1 !=null){ head.next = list1; } if(list2 !=null){ head.next = list2; } return root.next; }
(1) 创建额外存储数组 nums
(2) 依次循环遍历 pHead1, pHead2,将链表中的元素存储到 nums中,再对nums进行排序
(3) 依次对排序后的数组 nums取数并构建合并后的链表
public ListNode Merge(ListNode list1,ListNode list2) { // list1 list2为空的情况 if(list1==null) return list2; if(list2==null) return list1; if(list1 == null && list2 == null){ return null; } //将两个两个链表存放在list中 ArrayList<Integer> list = new ArrayList<>(); // 遍历存储list1 while(list1 !=null){ list.add(list1.val); list1 = list1.next; } // 遍历存储list2 while(list2 !=null){ list.add(list2.val); list2 = list2.next; } Collections.sort(list); // 将list转换为 链表 ListNode newHead = new ListNode(list.get(0)); ListNode cur = newHead; for(int i=1;i<list.size();i++){ cur.next = new ListNode(list.get(i)); cur = cur.next; } // 输出合并链表 return newHead; }
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。
输入: [{1,2},{1,4,5},{6}]
返回值: {1,1,2,4,5,6}
首当其冲可以用辅助数组方法,思路也很很简单,跟题目4差不多一致
根据第四题写的java,现在写的Python代码 def mergeKLists(self , lists: List[ListNode]) -> ListNode: newlist = [] for li in lists: while li: newlist.append(li.val) li = li.next; # print(newlist) ## 对新的newlist进行排序 newlist.sort() ## 生成新的链表 head = ListNode(-1) cur = head for i in range(len(newlist)): cur.next = ListNode(newlist[i]) cur = cur.next return head.next
时间复杂度O(NlogN):N表示列表中所有链表的结点数量,首先遍历所有结点O(N),排序O(NlogN)
空间复杂度O(N):辅助数组空间
使用两两合并的方法
public ListNode mergeKLists(ArrayList<ListNode> lists) { if (lists == null || lists.size() == 0) { return null; } return mergeSort(lists, 0, lists.size() - 1); } // 将链表数组二分 public ListNode mergeSort(ArrayList<ListNode> lists, int left, int right) { if (left >= right) { return lists.get(left); } int mid = left + ((right - left) >> 1); ListNode l1 = mergeSort(lists, left, mid); ListNode l2 = mergeSort(lists, mid + 1, right); return Merge(l1, l2); } public ListNode Merge(ListNode list1,ListNode list2) { if(list1 == null) return list2; if(list2 == null) return list1; if(list1.val <= list2.val){ list1.next = Merge(list1.next,list2); return list1; }else{ list2.next = Merge(list2.next,list1); return list2; } }
题目描述:判断给定的链表中是否有环。如果有环则返回true,否则返回false。
思路:双指针
public boolean hasCycle(ListNode head) { if(head == null) return false; // 定义快慢指针 ListNode fast = head; ListNode slow = head; while(fast !=null && fast.next !=null){ // 快指针是慢指针的两倍速度 fast = fast.next.next; slow = slow.next; // 如果两个指针相遇,那么就可成环 while(fast == slow){ return true; } } return false; }
题目描述:给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
方法1:hash法,记录第一次重复的结点
通过使用set或者map来存储已经遍历过的结点,当第一次出现重复的结点时,即为入口结点。
import java.util.HashSet; public class Solution { public ListNode EntryNodeOfLoop(ListNode pHead) { // 使用hash法,记录第一次重复的结点 HashSet<ListNode> hash = new HashSet<>(); // 存储的结点只要出现重复的,即为环的入口 while(pHead !=null){ if(hash.contains(pHead)){ return pHead; } // 添加未重复的结点 hash.add(pHead); pHead = pHead.next; } return null; } }
时间复杂度:O(n),需要将链表的所有结点遍历一遍,时间复杂度为O(n);
空间复杂度:O(n),需要额外的n空间的hashset来存储结点。
方法2:快慢指针,这个之前在leetcode快慢指针篇中介绍过
通过定义slow和fast指针,slow每走一步,fast走两步,若是有环,则一定会在环的某个结点处相遇(slow == fast),根据下图分析计算,可知从相遇处到入口结点的距离与头结点与入口结点的距离相同。
public ListNode EntryNodeOfLoop(ListNode pHead){ ListNode fast,slow; fast = slow = pHead; while(fast !=null && fast.next !=null){ slow = slow.next; fast = fast.next.next; // 记录快慢指针第一次相遇的结点 if(fast == slow) break; } // 如果快指针指向空,则不存在环 if(fast == null || fast.next ==null) return null; // 重新指向链表头部 fast = pHead; // 与第一次相遇的结点相同速度出发,相遇结点为入口结点 while(fast != slow){ fast = fast.next; slow = slow.next; } return fast; }
时间复杂度:O(n),需要将链表的所有结点遍历一遍,时间复杂度为O(n);
空间复杂度:O(1),需要额外两个快慢指针来遍历结点。
输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
输入:{1,2,3,4,5},2
返回值:{4,5}
说明:返回倒数第2个节点4,系统会打印后面所有的节点来比较。
方法1:使用辅助列表方法,先把所有输入值全保存在列表中,然后根据下标找到倒数第二的节点
public ListNode FindKthToTail (ListNode pHead, int k) {
// write code here
ArrayList<ListNode> list = new ArrayList<>();
while(pHead !=null){
list.add(pHead);
pHead = pHead.next;
}
if(k > list.size() || k == 0)
return null;
return list.get(list.size()-k);
}
方法2:快慢指针,让快指针先走k步,然后开始快慢指针同速前进,当快指针⾛到链表末尾 null 时,慢指针所在的位置就是倒数第 k个链表节点。
public ListNode FindKthToTail (ListNode pHead, int k) { // 使用快慢指针 if(pHead == null) return pHead; ListNode slow,fast; slow = fast = pHead; // 将快指针向后移动k个位置 while(k!=0){ //如果k值过大,导致快指针为空,则返回null if(fast == null) return null; fast = fast.next; k--; } //快慢指针开始一起移动 while(fast != null){ slow = slow.next; fast = fast.next; } return slow; }
在第8题的基础上进行修改
方法1:辅助列表
public ListNode removeNthFromEnd (ListNode head, int n) { // write code here ArrayList<ListNode> list = new ArrayList<>(); while(head !=null){ list.add(head); head = head.next; } if(n > list.size() || n == 0 || (list.size()==1 && n==1)) return null; list.remove(list.size()-n); //将list转换成链表 ListNode newHead = list.get(0); ListNode cur = newHead; for(int i=1;i<list.size();i++){ cur.next = list.get(i); cur = cur.next; } return newHead; }
方法二:快慢指针
(注意这里的slow代表的是倒数k个结点的前一个结点)
public ListNode removeNthFromEnd (ListNode head, int n) { // write code here if(head == null) return head; ListNode slow,fast; slow = fast = head; // 将快指针向后移动n个位置 for(int i=0;i<n;i++){ fast=fast.next; } //如果n的值等于链表的长度,直接返回去掉头结点的链表,所以是head.next if(fast == null) return head.next; //快慢指针开始一起移动 while(fast.next != null){ slow = slow.next; fast = fast.next; } // 将结点进行删除即可 slow.next = slow.next.next; return head; }
题目描述:输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。
方法一分析:双指针
1、使用两个指针N1,N2,一个从链表1的头节点开始遍历,我们记为N1,一个从链表2的头节点开始遍历,我们记为N2。
2、让N1和N2一起遍历,当N1先走完链表1的尽头(为null)的时候,则从链表2的头节点继续遍历,同样,如果N2先走完了链表2的尽头,则从链表1的头节点继续遍历,也就是说,N1和N2都会遍历链表1和链表2。
3、因为两个指针,同样的速度,走完同样长度(链表1+链表2),不管两条链表有无相同节点,都能够到达同时到达终点。(N1最后肯定能到达链表2的终点,N2肯定能到达链表1的终点)。
(判断条件)如何得到公共节点:
1、有公共节点的时候,N1和N2必会相遇,因为长度一样嘛,速度也一定,必会走到相同的地方的,所以当两者相等的时候,则会第一个公共的节点。
2、无公共节点的时候,此时N1和N2则都会走到终点,那么他们此时都是null,所以也算是相等了。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode l1 = pHead1, l2 =pHead2;
while(l1 != l2){
l1 =(l1 == null)? pHead2 :l1.next;
l2 =(l2 == null)? pHead1 :l2.next;
}
return l1;
}
方法二、用集合Set
具体步骤:
1、就是先把第一个链表的节点全部存放到集合set中。
2、然后遍历第二个链表的每一个节点,判断在集合set中是否存在,如果存在就直接返回这个存在的结点。
3、如果遍历完了,在集合set中还没找到,说明他们没有相交,直接返回null即可。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { // 创建集合set HashSet set = new HashSet<>(); //把链表1的结点放在set中 while(pHead1 !=null){ set.add(pHead1); pHead1 = pHead1.next; } //访问链表2 while(pHead2 !=null){ if(set.contains(pHead2)) return pHead2; pHead2 = pHead2.next; } return null; }
假设链表中每一个节点的值都在 0 - 9 之间,那么链表整体就可以代表一个整数。
给定两个这种链表,请生成代表两个整数相加值的结果链表。
输入:[9,3,7],[6,3]
返回值:{1,0,0,0}
思想:使用辅助栈
一个链表代表一个整数,返回多个链表的相加结果
故此顺位迭代就可以了,链表的问题大多借助栈和队列的结构思想
具体步骤:申请两个栈空间和一个标记位,然后将两个栈中内容依次相加,将结果倒插入新节点中。
public class Solution { /** * * @param head1 ListNode类 * @param head2 ListNode类 * @return ListNode类 */ public ListNode addInList (ListNode head1, ListNode head2) { // 创建两个栈list1和list2 LinkedList<Integer> list1 = new LinkedList<>(); //list1栈 LinkedList<Integer> list2 = new LinkedList<>(); //list2栈 putData(list1, head1); //入栈 putData(list2, head2); ListNode newNode = null; ListNode head = null; int carry = 0; //进位 while(!list1.isEmpty() || !list2.isEmpty() ||carry !=0){ int x = (list1.isEmpty()) ? 0: list1.pop(); int y = (list2.isEmpty()) ? 0: list2.pop(); int sum = x + y + carry; // j与进位相加 carry = sum/10; // 更新进位 //将计算值放入节点 newNode = new ListNode(sum %10); // 更新节点 newNode.next = head; head = newNode; } return head; } private static void putData(LinkedList<Integer> s1,ListNode head){ if(s1 == null) s1 = new LinkedList<>(); while(head !=null){ s1.push(head.val); head = head.next; } } } 时间复杂度:O(max(m,n)),由于只遍历了一次,只需要看两个链表更长的 空间复杂度:O(m+n),申请了两个栈来记录元素
主要通过辅助数组实现链表的排序
1、遍历链表并将链表结点存储到数组 tmp 中
2、通过对 tmp 进行排序,实现链表结点的排序
3、构建新链表结点 result,遍历数组 tmp ,拼接新的返回链表
public ListNode sortInList (ListNode head) { // write code here ArrayList<Integer> list = new ArrayList<>(); while(head !=null){ list.add(head.val); head = head.next; } Collections.sort(list); // 将list转换为 链表 ListNode newhead = new ListNode(list.get(0)); ListNode cur = newhead; for(int i=1;i<list.size();i++){ cur.next = new ListNode(list.get(i)); cur = cur.next; } return newhead; }
题目描述:给定一个链表,请判断该链表是否为回文结构。回文是指该字符串正序逆序完全一致。
示例1:输入:{2,1}
返回值:false
示例2: 输入:{1,2,2,1}
返回值:true
说明:1->2->2->1
1、首先初始化一个list列表;
2、遍历链表,将链表中的值转移至list中;
3、在list中通过比较头尾的值来判断链表是否为回文结构。
public boolean isPail (ListNode head) { // 辅助列表 if(head.next ==null) return true; List<Integer> list = new ArrayList<>(); //将链表转换成list while(head !=null){ list.add(head.val); head = head.next; } int j = list.size()-1; for(int i = 0; i<j; i++){ // 不适用equals而使用!=的话,对于有负数的测试用例可能会无法通过 // if(list.get(i) != list.get(j)){ // return false; // } if(!list.get(i).equals(list.get(j))){ return false; } j--; } return true; }
1、设置左右指针,left和right
2、然后分别开始进行比较左右指针的val值
3、满足要求,返回true,不满足,返回false
使用列表加快运算。
第一遍遍历链表分拣出奇偶结点的值,然后第二遍遍历把重新排序好的值形成新的链表即可
public ListNode oddEvenList (ListNode head) { // write code here if(head == null) return null; if(head.next == null) return head; ArrayList<Integer> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); int count = 0; while(head !=null){ count ++; if(count %2 !=0){ list1.add(head.val); }else{ list2.add(head.val); } head = head.next; } // 将列表进行重新连接 list1.addAll(list2); //构建新的链表 ListNode newHead = new ListNode(list1.get(0)); ListNode cur = newHead; for(int i=1;i<list1.size();i++){ cur.next = new ListNode(list1.get(i)); cur = cur.next; } return newHead; }
题目描述:删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次
例如:
给出的链表为1→1→2,返回1→2.
给出的链表为1→1→2→3→3,返回1→2→3.
解题思路:使用链表方法进行删除,因为是有序链表,两个重复的元素是相邻的,因此判断相邻元素是否相同,相同即直接删除即可。
public ListNode deleteDuplicates (ListNode head) {
// 采用链表的方法
if(head == null || head.next == null){
return head;
}
ListNode cur = head;
while(cur != null){
if(cur.next !=null && cur.val == cur.next.val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return head;
}
方法二:辅助列表
public ListNode deleteDuplicates (ListNode head) { // 采用辅助列表方法 if(head ==null){ return head; } ArrayList<Integer> list = new ArrayList<>(); // 判断重复元素 while(head !=null){ while(head.next !=null){ if(head.val != head.next.val) list.add(head.val); head = head.next; } list.add(head.val); head = head.next; } // 转换成链表 ListNode newHead = new ListNode(list.get(0)); ListNode cur = newHead; for(int i=1;i<list.size();i++){ cur.next = new ListNode(list.get(i)); cur = cur.next; } return newHead; }
题目描述:给出一个升序排序的链表,删除链表中的所有重复出现的元素,只保留原链表中只出现一次的元素。
例如:1→2→3→3→4→4→5, 返回1→2→5.
给出的链表为1→1→1→2→3, 返回2→3.
解题思路:这个跟15题很相像,但也有所区别,在这个里面我们需要引入一个pre指针,用来标记重复元素的前一个位置。
注意:头结点的情况也需要考虑到
public ListNode deleteDuplicates (ListNode head) { // 首先定义一个虚拟节点 ListNode dummy = new ListNode(0); // 将虚拟节点的下一个位置指向head dummy.next = head; ListNode pre = dummy; ListNode cur = head; while(cur != null && cur.next !=null){ if(cur.val != cur.next.val){ pre = cur; cur = cur.next; }else{ while(cur.next !=null && cur.val == cur.next.val){ // 找到值相同的最后一个 cur = cur.next; } // 删除值相同的节点 pre.next = cur.next; cur = cur.next; } } return dummy.next; }
方法二:递归
public ListNode deleteDuplicates (ListNode head) {
// 递归
if(head == null){
return null;
}
if(head.next != null && head.val == head.next.val){//发现有重复值
while(head.next != null && head.val == head.next.val){
head = head.next;//删除
}
return deleteDuplicates(head.next);//把与删除的那个结点相同的结点也进行删除
}
head.next = deleteDuplicates(head.next);//当没有发现重复值的情况,就一直进行递归操作
return head;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。