目录
目录
题目一:链表指定区间反转
题目描述
将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)。
例如:
给出的链表为 1\to 2 \to 3 \to 4 \to 5 \to NULL1→2→3→4→5→NULL, m=2,n=4m=2,n=4,
返回 1\to 4\to 3\to 2\to 5\to NULL1→4→3→2→5→NULL.
数据范围: 链表长度 0 < size \le 10000<size≤1000,0 < m \le n \le size0<m≤n≤size,链表中每个节点的值满足 |val| \le 1000∣val∣≤1000
要求:时间复杂度 O(n)O(n) ,空间复杂度 O(n)O(n)
进阶:时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)
思路解析
把节点进行一个简单的反转就可以
代码展示
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @param m int整型
* @param n int整型
* @return ListNode类
*/
public ListNode reverseBetween (ListNode head, int m, int n) {
// write code here
// write code here
ListNode res = new ListNode(-1);
res.next = head;
ListNode pre= res;
ListNode cur = head;
for (int i = 1; i < m; i++) {
pre = cur;
cur = cur.next;
}
for (int i = m; i > n; i--) {
ListNode temp = cur.next;
cur.next = temp.next;
temp.next = pre.next;
pre.next = temp;
}
return res.next;
}
}
题目二:链表中的节点每k个一组翻转
题目描述
将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表
如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样
你不能更改节点中的值,只能更改节点本身。
数据范围: \ 0 \le n \le 2000 0≤n≤2000 , 1 \le k \le 20001≤k≤2000 ,链表中每个元素都满足 0 \le val \le 10000≤val≤1000
要求空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)
例如:
给定的链表是 1\to2\to3\to4\to51→2→3→4→5
对于 k = 2k=2 , 你应该返回 2\to 1\to 4\to 3\to 52→1→4→3→5
对于 k = 3k=3 , 你应该返回 3\to2 \to1 \to 4\to 53→2→1→4→5
解题思路
现在我们想一想,如果拿到一个链表,想要像上述一样分组翻转应该做些什么?首先肯定是分段吧,至少我们要先分成一组一组,才能够在组内翻转,之后就是组内翻转,最后是将反转后的分组连接。
但是连接的时候遇到问题了:首先如果能够翻转,链表第一个元素一定是第一组,它翻转之后就跑到后面去了,而第一组的末尾元素才是新的链表首,我们要返回的也是这个元素,而原本的链表首要连接下一组翻转后的头部,即翻转前的尾部,如果不建立新的链表,看起来就会非常难。但是如果我们从最后的一个组开始翻转,得到了最后一个组的链表首,是不是可以直接连在倒数第二个组翻转后的尾(即翻转前的头)后面,这样从后往前是不是看起来就容易多了。
怎样从后往前呢?我们这时候可以用到自上而下再自下而上的递归或者说栈。接下来我们说说为什么能用递归?如果这个链表有nnn个分组可以反转,我们首先对第一个分组反转,那么是不是接下来将剩余n−1n-1n−1个分组反转后的结果接在第一组后面就行了,那这剩余的n−1n-1n−1组就是一个子问题。我们来看看递归的三段式模版:
- 终止条件: 当进行到最后一个分组,即不足k次遍历到链表尾(0次也算),就将剩余的部分直接返回。
- 返回值: 每一级要返回的就是翻转后的这一分组的头,以及连接好它后面所有翻转好的分组链表。
- 本级任务: 对于每个子问题,先遍历k次,找到该组结尾在哪里,然后从这一组开头遍历到结尾,依次翻转,结尾就可以作为下一个分组的开头,而先前指向开头的元素已经跑到了这一分组的最后,可以用它来连接它后面的子问题,即后面分组的头。
具体做法:
- step 1:每次从进入函数的头节点优先遍历链表k次,分出一组,若是后续不足k个节点,不用反转直接返回头。
- step 2:从进入函数的头节点开始,依次反转接下来的一组链表,反转过程同BM1.反转链表。
- step 3:这一组经过反转后,原来的头变成了尾,后面接下一组的反转结果,下一组采用上述递归继续。
代码展示
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode reverseKGroup (ListNode head, int k) {
/*
// write code here
// 根据长度来判断可以进行几轮
int length=numLength(head);
int num=length/k;
if(num<1)
return null;
ListNode prev=new ListNode(-1);
prev.next=head;
int i=0;
ListNode res=prev;
ListNode cur=prev.next;
while(i<num){
for(int j=1;j<k;j++){
ListNode temp=cur;
res.next=temp.next;
temp.next=prev.next;
prev.next=temp;
}
prev=cur.next;
cur.next=prev.next;
}
return head;
}
//获得链表的长度
public static int numLength(ListNode head){
int length=0;
while(head!=null){
length++;
head=head.next;
}
return length;
}*/
//找到每次翻转的尾部
ListNode tail = head;
//遍历k次到尾部
for (int i = 0; i < k; i++) {
//如果不足k到了链表尾,直接返回,不翻转
if (tail == null)
return head;
tail = tail.next;
}
//翻转时需要的前序和当前节点
ListNode pre = null;
ListNode cur = head;
//在到达当前段尾节点前
while (cur != tail) {
//翻转
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
//当前尾指向下一段要翻转的链表
head.next = reverseKGroup(tail, k);
return pre;
}
}
题目三 合并两个排序的链表
题目描述
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
数据范围: 0 ≤n≤1000,-1000 ≤节点值≤1000
要求:空间复杂度 O(1),时间复杂度 O(n)
解题思路
首先我们在分析题目的时候,我们最开始想到的就是使用一个链表来装载两个链表的结果,但是仔细看题目会发现什么? 空间复杂度为O(1),所以不允许添加新的链表进行,那么我们应该怎么做?
我们可以以一个链表为基准,比较两个链表的值,谁第一个元素小就插入在对应的链表上面!然后使用递归的方式一个一个的进行下去!
代码展示
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1, ListNode list2) {
//那么基本思路就是比较两个list的每一个元素,如果第一个list的元素小于第二个,那么久第一个往后走,然后第二个元素和第二个list的第一个比较,以此类推,直到最后排好序
//因为空间复杂度为O(1),所以不允许添加新的ListNode
if (list1 == null || list2 == null) {
return list1 != null ? list1 : list2;
}
// 两个链表元素依次对比
if (list1.val <= list2.val) {
// 递归计算 list1.next, list2
list1.next = Merge(list1.next, list2);
return list1;
} else {
// 递归计算 list1, list2.next
list2.next = Merge(list1, list2.next);
return list2;
}
}
}
题目四:合并K个排序的链表
题目描述
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。
数据范围:节点总数 0 n≤5000,每个节点的val满足 |val| <= 1000
要求:时间复杂度 O(nlogn)
解题思路
我们可以使用归并排序的方式进行求解,我们在求解两个链表合并的时候我们的想法是比较两个链表中的字符,谁小谁的next值就放下一个数据,对于多个链表来说,可以使用同样的方式,先将我们的链表划分到最小,然后使用二路归并的方式一层一层的往上走,直到最后;因此使用递归的方式就可以解决
代码展示
import java.util.ArrayList;
public class Solution {
//两个链表合并函数
public ListNode Merge(ListNode list1, ListNode list2) {
//一个已经为空了,直接返回另一个
if(list1 == null)
return list2;
if(list2 == null)
return list1;
//加一个表头
ListNode head = new ListNode(0);
ListNode cur = head;
//两个链表都要不为空
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;
}
//哪个链表还有剩,直接连在后面
if(list1 != null)
cur.next = list1;
else
cur.next = list2;
//返回值去掉表头
return head.next;
}
//划分合并区间函数
ListNode divideMerge(ArrayList<ListNode> lists, int left, int right){
if(left > right)
return null;
//中间一个的情况
else if(left == right)
return lists.get(left);
//从中间分成两段,再将合并好的两段合并
int mid = (left + right) / 2;
return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));
}
public ListNode mergeKLists(ArrayList<ListNode> lists) {
//k个链表归并排序
return divideMerge(lists, 0, lists.size() - 1);
}
}
题目五:判断表中是不是有环
题目描述
判断给定的链表中是否有环。如果有环则返回true,否则返回false。
数据范围:链表长度 0≤n≤10000,链表中任意节点的值满足 |val|<=100000
要求:空间复杂度 O(1),时间复杂度 O(n)
输入分为两部分,第一部分为链表,第二部分代表是否有环,然后将组成的head头结点传入到函数里面。-1代表无环,其它的数字代表有环,这些参数解释仅仅是为了方便读者自测调试。实际在编程时读入的是链表的头节点。
解题思路
这个题目我们使用双指针的方式进行求解
我们设置两个指针,快指针和慢指针
快指针每次走两步,慢指针每次走一步;如果快指针最后追上了慢指针,就证明有环,如果没有追上就证明不存在环!
代码展示
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
//我们可以给每个节点添加一个访问状态,如果我们在找寻下一个节点的时候找寻到已经被访问了,那么就不正确
//但是把,使用这张方式会有空间复杂度
/*我们还可以使用双指针的方式进行,我们设置一个快指针,一个慢指针,快指针一次走两步,慢指针一次走一步
如果最后快指针追上了慢指针,就证明存在环,否则就证明不存在
*/
//首先判断输入进来的链表有没有值,没有就可以直接返回false,因为没有值肯定没有环
if(head==null) return false;
//设置快指针
ListNode fast=head;
//设置慢指针
ListNode slow=head;
while(fast!=null && fast.next!=null){
fast=fast.next.next; //一次走两步
slow=slow.next; //一次走一步
if(fast==slow) return true; //如果相遇了,就返回true,证明有环
}
return false;
}
}
题目六:链表中环的入口节点
题目描述
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围: n≤10000,1<=结点值<=10000
要求:空间复杂度 O(1),时间复杂度 O(n)
解题思路
那我们现在假定已经是一个有环的链表了,那么这个链表中怎么找到环的入口呢?在慢指针进入链表环之前,快指针已经进入了环,且在里面循环,这才能在慢指针进入环之后,快指针追到了慢指针,不妨假设快指针在环中走了nnn圈,慢指针在环中走了mmm圈,它们才相遇,而进入环之前的距离为xxx,环入口到相遇点的距离为yyy,相遇点到环入口的距离为zzz。快指针一共走了x+n(y+z)+y步,慢指针一共走了x+m(y+z)+y,这个时候快指针走的倍数是慢指针的两倍,则x+n(y+z)+y=2(x+m(y+z)+y),这时候x+y=(n−2m)(y+z),因为环的大小是y+z,说明从链表头经过环入口到达相遇地方经过的距离等于整数倍环的大小:那我们从头开始遍历到相遇位置,和从相遇位置开始在环中遍历,会使用相同的步数,而双方最后都会经过入口到相遇位置这y个节点,那说明这y个节点它们就是重叠遍历的,那它们从入口位置就相遇了,这我们不就找到了吗?
代码展示
public class Solution {
//判断有没有环,返回相遇的地方
public ListNode hasCycle(ListNode head) {
//先判断链表为空的情况
if (head == null)
return null;
//快慢双指针
ListNode fast = head;
ListNode slow = head;
//如果没环快指针会先到链表尾
while (fast != null && fast.next != null) {
//快指针移动两步
fast = fast.next.next;
//慢指针移动一步
slow = slow.next;
//相遇则有环,返回相遇的位置
if (fast == slow)
return slow;
}
//到末尾说明没有环,返回null
return null;
}
public ListNode EntryNodeOfLoop(ListNode pHead) {
ListNode slow = hasCycle(pHead);
//没有环
if (slow == null)
return null;
//快指针回到表头
ListNode fast = pHead;
//再次相遇即是环入口
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
题目七 链表中倒数最后K个节点
题目描述
解题思路
我们可以使用双指针的方式进行
快指针快于慢指针K步
首先如果快指针达不到快于慢指针K步的要求就直接退出,因为不存在K个节点
然后如果可以达到要求,我们使得快指针快于慢指针K步,然后快指针指向null的时候,慢指针正好开始指向倒数第K个节点的起始位置了,然后我们就可以返回了!!
代码展示
import java.util.*;
public class Solution {
public ListNode FindKthToTail (ListNode pHead, int k) {
int n = 0;
ListNode fast = pHead;
ListNode slow = pHead;
//快指针先行k步
for (int i = 0; i < k; i++) {
if (fast != null)
fast = fast.next;
//达不到k步说明链表过短,没有倒数k
else
return slow = null;
}
//快慢指针同步,快指针先到底,慢指针指向倒数第k个
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
题目八 删除链表的倒数第N个节点
题目描述
给定一个链表,删除链表的倒数第 n 个节点并返回链表的头指针
例如,
给出的链表为: 1→2→3→4→5, n=2.
删除了链表的倒数第 n 个节点之后,链表变为1→2→3→5.
解题思路
我们还是使用双指针的方式,最开始我的思路错误的原因就在于我太局限了,其实可以为链表添加一个头,然后使用三个指针来进行操作,快指针和慢指针还有一个指向慢指针的前面一个节点的指针,这样当慢指针到达第n个节点的时候,pre指针正好到达慢指针的前面一个节点,这个时候只要使用pre.next=慢指针.next就可以求解了!就删除了节点了!!
代码展示
import java.util.*;
public class Solution {
public ListNode removeNthFromEnd (ListNode head, int n) {
//添加表头
ListNode res = new ListNode(-1);
res.next = head;
//当前节点
ListNode cur = head;
//前序节点
ListNode pre = res;
ListNode fast = head;
//快指针先行n步
while(n != 0){
fast = fast.next;
n--;
}
//快慢指针同步,快指针到达末尾,慢指针就到了倒数第n个位置
while(fast != null){
fast = fast.next;
pre = cur;
cur = cur.next;
}
//删除该位置的节点
pre.next = cur.next;
//返回去掉头
return res.next;
}
}
题目九 两个链表的第一个公共节点
题目描述
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
解题思路
这个题目我们还是可以使用双指针的方式进行求解
我们使得两个指针分别指向不一样的链表的头部,然后依次往后遍历,需要找寻两个链表的相交点,如果走到最后也没找到,此链表上面的指针就会指向另外一个链表的头部,然后继续开始找,只用两轮就一定可以找到相交点!!
代码展示
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
/* 什么是首尾相接法?
我们指定两个指针,一个指针从head1开始,一个从head2开始,然后往后面找
因为两个长度肯定不相等,所以一定会有一个先到达末尾,到达末尾之后我们就使得这个指针指向另外一个链表的头
最后两个指针相遇的时候就可以得到我们最终想要的结果了
*/
ListNode p1=pHead1;
ListNode p2=pHead2;
while(p1!=p2){
p1=p1==null?pHead2:p1.next;
p2=p2==null?pHead1:p2.next;
}
return p1;
}
}
题目十 链表相加(二)
题目描述
解题思路
我们使用两个栈,将数据分别存储在两个栈中,然后从栈中依次拿去第一个数据,并定义一个carry标志,如果两个栈相加得到的结果除以10大于0,就表示需要进一位,然后取余,将两个值求余得结果放在当前位上!最后就可以得到我们的结果了!!
代码展示
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head1 ListNode类
* @param head2 ListNode类
* @return ListNode类
*/
public ListNode addInList (ListNode head1, ListNode head2) {
// write code here
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) { //carry!=0就保证了最后一次如果还有数据的话可以保证最后一次可以获取到
int x = (list1.isEmpty()) ? 0 : list1.pop(); //依次从栈中取出
int y = (list2.isEmpty()) ? 0 : list2.pop();
int sum = x + y + carry; //与进位一起相加
carry = sum / 10; //更新进位
//将计算值放入节点
newNode = new ListNode(sum % 10);
//更新下一个节点的指向
newNode.next = head;
head = newNode;
}
return head;
}
private static void putData(LinkedList<Integer> s1, ListNode head1) {
if (s1 == null) s1 = new LinkedList<>();
//遍历节点将其插入栈中
while (head1 != null) {
s1.push(head1.val);
head1 = head1.next;
}
}
}
题目十一 单链表的排序
题目描述
给定一个节点数为n的无序单链表,对其按升序排序。
数据范围:0 < n≤100000
要求:空间复杂度 O(n),时间复杂度 O(nlogn)
解题思路
- step 1:首先判断链表为空或者只有一个元素,直接就是有序的。
- step 2:准备三个指针,快指针right每次走两步,慢指针mid每次走一步,前序指针left每次跟在mid前一个位置。三个指针遍历链表,当快指针到达链表尾部的时候,慢指针mid刚好走了链表的一半,正好是中间位置。
- step 3:从left位置将链表断开,刚好分成两个子问题开始递归。
- step 4:将子问题得到的链表合并,参考合并两个有序链表。
代码展示
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
// write code here
//使用双指针进行求解
/*首先添加一个头节点,然后p1指向head,p2指向head.next 然后比较两个结点的值,如果p2<p1的话将P2节点向前移动
以此类推,当p2到达末尾得时候,p1指针向后移动,然后p2节点重新指向p1的后一个节点,直到最后p1指向null
当然也可以进行优化,设置一个变量记录节点上一次没有变化的位置,下一次p1直接进行跳跃就行
*/
public class Solution {
//合并两段有序链表
ListNode merge(ListNode pHead1, ListNode pHead2) {
//一个已经为空了,直接返回另一个
if (pHead1 == null)
return pHead2;
if (pHead2 == null)
return pHead1;
//加一个表头
ListNode head = new ListNode(0);
ListNode cur = head;
//两个链表都要不为空
while (pHead1 != null && pHead2 != null) {
//取较小值的节点
if (pHead1.val <= pHead2.val) {
cur.next = pHead1;
//只移动取值的指针
pHead1 = pHead1.next;
} else {
cur.next = pHead2;
//只移动取值的指针
pHead2 = pHead2.next;
}
//指针后移
cur = cur.next;
}
//哪个链表还有剩,直接连在后面
if (pHead1 != null)
cur.next = pHead1;
else
cur.next = pHead2;
//返回值去掉表头
return head.next;
}
public ListNode sortInList (ListNode head) {
//链表为空或者只有一个元素,直接就是有序的
if (head == null || head.next == null)
return head;
ListNode left = head;
ListNode mid = head.next;
ListNode right = head.next.next;
//右边的指针到达末尾时,中间的指针指向该段链表的中间
while (right != null && right.next != null) {
left = left.next;
mid = mid.next;
right = right.next.next;
}
//左边指针指向左段的左右一个节点,从这里断开
left.next = null;
//分成两段排序,合并排好序的两段
return merge(sortInList(head), sortInList(mid));
}
}
题目十二 删除有序链表中的重复元素Ⅰ
题目描述
删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次
例如:
给出的链表为1→1→2,返回1→2.
给出的链表为1→1→2→3→3,返回1→2→3.
数据范围:链表长度满足 0≤n≤100,链表中任意节点的值满足 |val|≤100
进阶:空间复杂度 O(1),时间复杂度 O(n)
解题思路
还是使用双指针进行求解
一个在前面一个在后面,如果p2不等于p1 p1和p2同时向后走 如果相等的话就移动元素位置来做到删除元素
代码展示
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
// write code here
//还是可以使用双指针进行求解
/**第一个指针指在前面,第二个指针往后走,如果两个指针的值相等的话,将指针的指向进行改变
然后删除完一个节点之后,p1指针往后移,p2指针也往后移动
依次类推,得到最后的链表
*/
if (head == null) return null;
if(head.next==null) return head;
ListNode p1 = head;
ListNode p2 = head.next;
while ( p2 != null) {
if (p1.val == p2.val) {
p1.next = p2.next;
p2 = p2.next;
} else {
p1 = p1.next;
p2 = p2.next;
}
}
return head;
}
}
题目十三 删除链表中的重复元素Ⅱ
题目描述
解题思路
- step 1:给链表前加上表头,方便可能的话删除第一个节点。
1 2 3 |
|
- step 2:遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去。
- step 3:在step 2中这一连串相同的节点前的节点直接连上后续第一个不相同值的节点。
- step 4:返回时去掉添加的表头。
代码展示
import java.util.*;
public class Solution {
public ListNode deleteDuplicates (ListNode head) {
//空链表
if (head == null)
return null;
ListNode res = new ListNode(0);
//在链表前加一个表头
res.next = head;
ListNode cur = res;
while (cur.next != null && cur.next.next != null) {
//遇到相邻两个节点值相同
if (cur.next.val == cur.next.next.val) {
int temp = cur.next.val;
//将所有相同的都跳过
while (cur.next != null && cur.next.val == temp)
cur.next = cur.next.next;
} else
cur = cur.next;
}
//返回时去掉表头
return res.next;
}
}