BM1 反转链表
题目描述
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围: 0≤n≤1000
要求:空间复杂度 O(1) ,时间复杂度 O(n)。
如当输入链表{1,2,3}时,
经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。
以上转换过程如下图所示:
题解
反转链表的基本思想就是利用三个指针,pre,cur,nex分别表示当前节点前一个节点、当前节点以及当前节点后面的节点,之后再令cur.next = pre;其实就完成了反转操作,完成反转操作之后将三个指针依次后移即可。
Java代码实现
public class Solution {
public ListNode ReverseList(ListNode head) {
// 优先考虑特殊情况,一个节点都没有或是只有一个节点,直接返回即可
if(head == null || head.next == null){
return head;
}
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode nex = cur.next;
// 这里是反转的核心,令之前在前面的指针挪到后面
cur.next = pre;
// 之后依次将各指针后移即可;
pre = cur;
cur = nex;
}
//当退出循环时,其实就是cur == null的情况,这时
return pre;
}
}
BM2 链表内指定区间反转
题目描述
将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O ( n*),空间复杂度 O(1)。 例如: 给出的链表为 1→2→3→4→5→NULL, m=2,n=4m=2,n=4, 返回 1→4→3→2→5→NULL.
数据范围: 链表长度 0<size≤1000,0<m≤n≤size,链表中每个节点的值满足 |val|≤1000|val|≤1000
要求:时间复杂度 O(n) ,空间复杂度 O(n)
进阶:时间复杂度 O(n) ,空间复杂度 O(1)
题解
这题的思想和反转链表实际上是一样的,关键点(与反转整个链表不同之处)在于这里反转的链表只是整个链表中的一部分,故涉及链表的截断(cur.next = null)以及连接(cur.next = head2.next),考虑方法如下:首先需要将链表的头尾指针进行保存(考虑如下几个指针的保存,原链表截断之前的指针,截断部分的头尾指针以及截断部分之后的头指针),之后将截断部分进行反转,之后再进行链表的拼接即可。
可用一个虚拟头节点防止头节点各种复杂条件判断
Java代码实现
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
// 首先考虑特殊情况,若是m == n即有一个节点需要反转,那直接无需反转
if(m == n){
return head;
}
// 首先考虑需要的指针,总共有三条链表,用head1-3以及tail1-3分别表示其头尾
ListNode head2 = null;
ListNode head3 = null;
ListNode tail1 = null;
ListNode tail2 = null;
// 此外,链表遍历还用到两个指针
ListNode pre = null;
ListNode cur = head;
// 考虑利用虚指针
ListNode dummy = new ListNode(-1);
dummy.next = head;
pre = dummy;
// 用于标记索引下标
int i = 1;
while(cur != null){
if(i == m){
//将链表截断
pre.next = null;
tail1 = pre;
head2 = cur;
}else if(i == n + 1){
//将链表截断
pre.next = null;
tail2 = pre;
pre.next = null;
// 这里可能只有n个节点,但此时到了第i + 1个节点,为null
if(cur != null){
head3 = cur;
}
}
// 无论是否需要截断链表都将指针后移
pre = cur;
cur = cur.next;
++i;
}
// 将其反转拼接第一二段
tail1.next = reverse(head2);
// 拼接二三段
head2 = tail1;
while(head2.next != null){
head2 = head2.next;
}
head2.next = head3;
// 这里的拼接过程,我的原本是,没有连接上,若是要连接链表,需要通过next下标
// while(tail1 != null){
// tail1 = tail1.next;
// }
// tail1 = head3;
return dummy.next;
}
// 这里返回反转后的头指针
private ListNode reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode nex = cur.next;
cur.next = pre;
pre = cur;
cur = nex;
}
return pre;
}
}
BM3 链表中的节点每k个一组翻转
题目描述
将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表 如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样 你不能更改节点中的值,只能更改节点本身。
数据范围:0≤n≤2000 ,1≤k≤2000 ,链表中每个元素都满足 0≤val≤1000
要求空间复杂度O(1) ,时间复杂度、O(n)
例如:
给定的链表是1→2→3→4→5
对于k=2 , 你应该返回2→1→4→3→5
对于k=3 , 你应该返回 3→2→1→4→5
题解
先求出链表的长度,然后判断需要分几组,之后对每组进行反转并连接,过程中需要注意保存以下节点:preStart-->需要反转的节点之前的节点,start-->开始反转的节点,afterStart-->反转结束的下一个节点,end-->反转结束的节点,还有就是反转链表需要的三个节点pre,cur以及nex。
可用一个虚拟头节点防止头节点各种复杂条件判断
注意点:每次连接时只连接一端(连接上一个链表和这一个链表),当全部反转都完成之后,再连接这一个链表和下一个链表
Java代码实现
public class Solution {
/**
*
* @param head ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode reverseKGroup (ListNode head, int k) {
// write code here
// 特殊情况,每1个为1组进行反转,直接返回即可
if(k == 1){
return head;
}
// 长度
int len = 0;
// 虚拟头节点
ListNode dummy = new ListNode(-1);
dummy.next = head;
// 反转节点之前的节点
ListNode preStart = null;
// 开始反转的节点(反转之后变成了需要和后面链表进行拼接的节点)
ListNode start = dummy;
// 反转结束的节点
ListNode end = null;
// 反转结束节点之后的节点
ListNode afterStart = null;
// 反转过程中需要的节点
ListNode pre = dummy;
ListNode cur = head;
ListNode nex = null;
while(cur != null){
++len;
cur = cur.next;
}
// 判断需要分几组
int group = len / k;
// 特殊情况,head为空或为1直接返回
if(len == 0 || len == 1 || group < 1){
return head;
}
cur = head;
for(int i = 0; i < group; ++i){
// 先记录反转之前的节点以及开始节点
preStart = start; // 反转之前的节点实际上是上一反转链表的头(第一次反转,其前一个节点为dummy,故对preStart赋初值应为dummy)
start = cur;
for(int j = 0; j < k; ++j){
nex = cur.next;
// 开始进行反转
cur.next = pre;
pre = cur;
cur = nex;
}
end = pre;
afterStart = cur;
// 开始进行连接,注意这里只是连接反转之前的头和这一次的尾
preStart.next = end;
}
// 当反转全部完成时,再将这一次反转的尾和反转之后的节点进行连接
start.next = afterStart;
return dummy.next;
}
}
BM4 合并两个排序的链表
题目描述
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
数据范围: 0≤n≤10000,−1000≤节点值≤1000 要求:空间复杂度 O(1),时间复杂度 O(n)
题解
双指针移动即可,但若是想达到O(1)的时间复杂度,则需要原地修改指针,故选择如下方法:将两个链表中的当前节点标记,标记当前节点的作用是,判断本次应该连接哪一个节点(链表1中的节点或是链表2中的节点),之后再将剩下的链表合并即可。
可用一个虚拟头节点防止头节点各种复杂条件判断
Java代码实现
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
// 首先考虑特殊情况,即两个链表中一个链表为空时,直接返回另一个链表即可
if(list1 == null || list2 == null){
return list1 == null ? list2 : list1;
}
// 首先需要两个指针分别记录两个链表走到了哪
ListNode cur1 = list1;
ListNode cur2 = list2;
// 避免考虑头节点,先来个虚拟节点
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
// 先将两个链表合并
while(cur1 != null && cur2 != null){
if(cur1.val < cur2.val){
cur.next = cur1;
cur1 = cur1.next;
}else{
cur.next = cur2;
cur2 = cur2.next;
}
cur = cur.next;
}
// 再合并剩下的部分,此时一个链表已经为空了
cur.next = cur1 == null ? cur2 : cur1;
return dummy.next;
}
}
BM5 合并k个已排序的链表
题目描述
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。
数据范围:节点总数 0≤n≤5000,每个节点的val满足 ∣val∣<=100
要求:时间复杂度 O(nlogn)
题解
这题只说了要求时间复杂度O(nlogn),而未要求空间复杂度,我自己的想法是将链表中所有节点的值保存到ArrayList中进行排序,排序之后再依次生成节点即可。
Java代码实现
public class Solution {
public ListNode mergeKLists(ArrayList<ListNode> lists) {
int n = lists.size();
// 当前节点
ListNode cur = null;
// 存储全部元素所用的集合
ArrayList<Integer> res = new ArrayList<>();
for(int i = 0; i < n; ++i){
// 将每个链表的每个节点都存入
cur = lists.get(i);
while(cur != null){
res.add(cur.val);
cur = cur.next;
}
}
Collections.sort(res);
// 建一个虚拟头指针已防止头节点的误判
ListNode dummy = new ListNode(0);
cur = dummy;
for(Integer num : res){
cur.next = new ListNode(num);
cur = cur.next;
}
return dummy.next;
}
}
但是注意以上题解并未使用到一个条件-->这n个链表均为有序链表,因此时间复杂度肯定不是最佳,想到用k个指针进行迭代,但经过k - 1次比较之后才能完成1个数的排序,故其时间复杂度为O(kn),由此想到维护一个一直有序的数组,每次取出下标为0的位置,并将该数所在链表的后一个节点放入数组中(保证其有序-->这里可以使用二分找到对应的下标),但此时出现矛盾,使用二分进行查找,那数组更合适,但若是涉及插入操作,则链表更合适,,,矛盾,故舍弃此思路。
但看题解是发现大部分此思路采用的是优先队列来实现,队列自动维护,每次只需取出队头就行。
看了题解后发现:不是使用k个指针,而是每次合并的时候是两两合并,如有链表1, 2, 3, 4, 5, 6, 7, 8,第一次合并,将12合并,34合并,56合并,78合并,得到四个新链表1, 2, 3, 4,之后再次合并,12合并,34合并,得到链表1, 2,最后再将12合并得到所需的链表,完整题解如下:
题解
采用分治法的思想(先分后并),将所给的k个链表分成两份,一直持续地分,分到不能再分为止(k个链表被分为k份),之后再依次将两个链表进行合并,直到合并为最后一个链表为止。
Java代码实现
public class Solution {
public ListNode mergeKLists(ArrayList<ListNode> lists) {
int n = lists.size();
// 此函数确定需要合并的下标范围0----n-1
return merge(lists, 0, n - 1);
}
// 这个函数的作用是从left合并到right
private ListNode merge(ArrayList<ListNode> lists, int left, int right){
// 若是无法再分,则直接返回
if(left == right){
return lists.get(left);
// left > right表示长度为0,即merge(lists, 0, -1),数组中有任何一个链表为空时都会产生此情况
}else if(left > right){
return null;
}else{
int mid = (left + right) / 2;
// 否则考虑合并left, mid为一个链表,mid +1 到right为一个链表
return merge2Lists(merge(lists, left, mid), merge(lists, mid + 1, right));
}
}
// 这一部分完全就是合并两个链表的代码
private ListNode merge2Lists(ListNode list1, ListNode list2){
// 首先考虑特殊情况,即两个链表中一个链表为空时,直接返回另一个链表即可
if(list1 == null || list2 == null){
return list1 == null ? list2 : list1;
}
// 首先需要两个指针分别记录两个链表走到了哪
ListNode cur1 = list1;
ListNode cur2 = list2;
// 避免考虑头节点,先来个虚拟节点
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
// 先将两个链表合并
while(cur1 != null && cur2 != null){
if(cur1.val < cur2.val){
cur.next = cur1;
cur1 = cur1.next;
}else{
cur.next = cur2;
cur2 = cur2.next;
}
cur = cur.next;
}
// 再合并剩下的部分,此时一个链表已经为空了
cur.next = cur1 == null ? cur2 : cur1;
return dummy.next;
}
}
BM6 判断链表中是否有环
题目描述
判断给定的链表中是否有环。如果有环则返回true,否则返回false。
数据范围:链表长度 0≤n≤10000,链表中任意节点的值满足 ∣val∣<=100000
要求:空间复杂度 O(1),时间复杂度 O(n)
输入分为两部分,第一部分为链表,第二部分代表是否有环,然后将组成的head头结点传入到函数里面。-1代表无环,其它的数字代表有环,这些参数解释仅仅是为了方便读者自测调试。实际在编程时读入的是链表的头节点。
例如输入{3,2,0,-4},1时,对应的链表结构如下图所示:
可以看出环的入口结点为从头结点开始的第1个结点(注:头结点为第0个结点),所以输出true。
题解
快慢指针,一个每次走一格,一个每次走两个,若是有环则必会相遇,这里题目中没说任意节点的值均不同,判断两指针是否相遇时最好不只是判断两个节点的val是否相遇而是判断两个指针的地址是否相同
Java代码实现
public class Solution {
public boolean hasCycle(ListNode head) {
// 这里注意题目中说0 <= n,故有可能为0,特殊情况优先考虑
// 由于之后的fast和slow都是先赋值,为了防止对空指针进行操作,需要先进行判断
if(head == null || head.next == null || head.next.next == null){
return false;
}
ListNode slow = head.next;
ListNode fast = head.next.next;
// 快慢指针,若是有一个先遍历完,那必然是快指针
while(fast != null){
// 这里是先比较后移动,因为第一次移动过了
if(slow == fast){
return true;
}
slow = slow.next;
// 增加这个判断是防止当fast到了最后一个元素时,fast.next.next就对空指针进行了操作
// 也可以将判断条件加在while循环中,即:while(fast != null && fast.next != null)
if(fast.next != null){
fast = fast.next.next;
}else{
return false;
}
}
return false;
}
}
BM7 链表中环的入口节点
题目描述
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围: n≤10000,1<=结点值<=10000
要求:空间复杂度 O(1),时间复杂度 O(n)
例如,输入{1,2},{3,4,5}时,对应的环形链表如下图所示:
可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。
输入描述
输入分为2段,第一段是入环前的链表部分,第二段是链表环的部分,后台会根据第二段是否为空将这两段组装成一个无环或者有环单链表
返回值描述
返回链表的环的入口结点即可,我们后台程序会打印这个结点对应的结点值;若没有,则返回对应编程语言的空结点即可。
题解
这题的做法和判断是否有环类似,都是先使用快慢指针找到两个指针重合的位置判定有环,之后注意一个点,此时将其中一个指针移至原链表头并让两个指针以相同的速度移动,那么当两个指针再次相遇时的节点即为入口节点,具体证明如下,图可参考官方题解:
首先假设链表中无环的部分(从头节点到环的入口节点)距离为x,从环的入口到相遇节点的距离为y,从相遇节点再次到环的入口距离为m,则由快慢指针的速度可得如下距离方程:
(x + y + m + y) / (x + y) = 2
其中除号左边为快指针走的距离,除号右边为慢指针走的距离,两者走了相同的时间,距离刚好与速度成正比
Java代码实现
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
// 特殊情况优先考虑
if(pHead == null || pHead.next == null){
return null;
}
ListNode fast = pHead.next.next;
ListNode slow = pHead.next;
while(fast != null && fast.next != null){
if(fast == slow){
// 此时有环,开始第二轮遍历
fast = pHead;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
fast = fast.next.next;
slow = slow.next;
}
// 退出了循环证明无环
return null;
}
}
BM8 链表中倒数最后k个节点
题目描述
输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
数据范围:0≤n≤105,0≤ai≤109,0≤k≤109
要求:空间复杂度 O(n),时间复杂度 O(n)
进阶:空间复杂度 O(1),时间复杂度 O(n)
例如输入{1,2,3,4,5},2时,对应的链表结构如下图所示:
其中蓝色部分为该链表的最后2个结点,所以返回倒数第2个结点(也即结点值为4的结点)即可,系统会打印后面所有的节点来比较。
题解
两次遍历链表,第一次求出链表长度,第二次直接返回最后k个节点的开始节点
Java代码实现
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode FindKthToTail (ListNode pHead, int k) {
// write code here
int len = 0;
ListNode cur = pHead;
while(cur != null){
++len;
cur = cur.next;
}
// 题目中说若是链表长度小于k则返回空链表,这里已经考虑了链表为空的情况(len = 0)
if(len < k){
return null;
}
cur = pHead;
int i = 0;
// 这里需要稍微考虑下i++到底应该小于多少
while(i++ < len - k){
cur = cur.next;
}
return cur;
}
}
BM9 删除链表的倒数第n个节点
题目描述
给定一个链表,删除链表的倒数第 n 个节点并返回链表的头指针 例如,
给出的链表为: 1→2→3→4→5, n=2. 删除了链表的倒数第 nn 个节点之后,链表变为1→2→3→5.
数据范围: 链表长度 0≤n≤1000,链表中任意节点的值满足 0≤val≤100
要求:空间复杂度 O(1),时间复杂度 O(n) 备注:
题目保证 n 一定是有效的
题解
这里的思路也是两次遍历链表,然后找到需要删除的节点,但要注意保存需要删除的节点之前的节点。
可用一个虚拟头节点防止各种复杂条件判断
Java代码实现
public class Solution {
/**
*
* @param head ListNode类
* @param n int整型
* @return ListNode类
*/
public ListNode removeNthFromEnd (ListNode head, int n) {
// write code here
int len = 0;
// 虚拟头节点
ListNode dummy = new ListNode(-1);
dummy.next = head;
// 遍历需要的两个节点
ListNode cur = head;
ListNode pre = dummy;
// 开始第一次遍历求出链表长度
while(cur != null){
++len;
cur = cur.next;
}
// 题目中说n符合要求,故无需判断,直接二次遍历即可
int i = 0;
//注意这里将cur指针返回head
cur = head;
while(i++ < len - n){
pre = cur;
cur = cur.next;
}
// 这里将cur节点删除,实际上就是将cur之前的指针与cur之后的指针进行连接即可
pre.next = cur.next;
return dummy.next;
}
}
BM10 两个链表的第一个公共结点
题目描述
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
数据范围: n≤1000 要求:空间复杂度 O(1),时间复杂度 O(n)
例如,输入{1,2,3},{4,5},{6,7}时,两个无环的单向链表的结构如下图所示:
可以看到它们的第一个公共结点的结点值为6,所以返回结点值为6的结点。
输入描述:
输入分为是3段,第一段是第一个链表的非公共部分,第二段是第二个链表的非公共部分,第三段是第一个链表和第二个链表的公共部分。 后台会将这3个参数组装为两个链表,并将这两个链表对应的头节点传入到函数FindFirstCommonNode里面,用户得到的输入只有pHead1和pHead2。
返回值描述:
返回传入的pHead1和pHead2的第一个公共结点,后台会打印以该节点为头节点的链表。
题解
最简单的方法是先将一个链表中的所有节点放入一个set中,之后对于第二个链表中的每一个节点,若是set中已有,则返回该节点(需要的是第一个公共节点,故只要找到就直接返回),若是遍历完之后仍然没有找到公共节点,那么返回null,但此方法空间复杂度为O(n),不合题意,若是想要O(1)的时间复杂度可采取如下思路:
两次遍历链表,第一次遍历记载两个链表的长度,然后找到一个链表比另一个链表长多少(len1 - len2),较长的链表先走(len1 - len2)格,之后两个链表一起走,若是某时刻两个节点相同,那直接返回,之后若是遍历完仍然未找到相同元素,返回null
Java代码实现
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
// 特殊情况,若是两个链表某一个为空,那直接返回null
if(pHead1 == null || pHead2 == null){
return null;
}
int len1 = 0;
int len2 = 0;
// 当前链表1走到了哪
ListNode cur1 = pHead1;
// 当前链表2走到了哪
ListNode cur2 = pHead2;
while(cur1 != null || cur2 != null){
if(cur1 != null){
++len1;
cur1 = cur1.next;
}
if(cur2 != null){
++len2;
cur2 = cur2.next;
}
}
// 将指针放回头部
cur1 = pHead1;
cur2 = pHead2;
if(len1 >= len2){
return run(cur1, cur2, len1 - len2);
}else{
return run(cur2, cur1, len2 - len1);
}
}
private ListNode run(ListNode head1, ListNode head2, int lenSub){
// 先让head1走lenSub格,已经确保head1不比head2短
int i = 0;
while(i++ < lenSub){
head1 = head1.next;
}
// 之后两个一起走
while(head1 != null && head2 != null){
if(head1 == head2){
return head1;
}
head1 = head1.next;
head2 = head2.next;
}
// 要是退出循环(遍历完)还未找到说明不存在
return null;
}
}
BM11 链表相加(二)
题目描述
假设链表中每一个节点的值都在 0 - 9 之间,那么链表整体就可以代表一个整数。
给定两个这种链表,请生成代表两个整数相加值的结果链表。
数据范围:0≤n,m≤1000000,链表任意值 0≤val≤9 要求:空间复杂度 O(n),时间复杂度 O(n)
例如:链表 1 为 9->3->7,链表 2 为 6->3,最后生成新的结果链表为 1->0->0->0。
题解
最简单的方法,将链表中的每一个元素取出来,两个数相加之后再返回,但是注意数据范围是0≤n,m≤1000000,此方法无法保存这么大的数据,故舍弃。
考虑直接对链表节点进行相加,但涉及两个问题,一是顺序问题(链表是正序的,但是相加时需要逆序),而是考虑进位问题,对于顺序问题,先进后出考虑用栈解决,进位只需多一个节点进行保存即可。
Java代码实现
public class Solution {
/**
*
* @param head1 ListNode类
* @param head2 ListNode类
* @return ListNode类
*/
public ListNode addInList (ListNode head1, ListNode head2) {
// write code here
// 特殊情况优先考虑,若是两个链表一个为空那直接返回另一个即可
if(head1 == null || head2 == null){
return head1 == null ? head2 : head1;
}
// 栈中存节点空间消耗太大,直接存节点中的值即可
// 保存第一个链表中的数
Stack<Integer> stack1 = new Stack<>();
// 保存第二个链表中的数
Stack<Integer> stack2 = new Stack<>();
// 保存相加之后的结果
Stack<Integer> ans = new Stack<>();
ListNode cur = head1;
while(cur != null){
stack1.push(cur.val);
cur = cur.next;
}
cur = head2;
while(cur != null){
stack2.push(cur.val);
cur = cur.next;
}
// 进位
int carry = 0;
while(!stack1.isEmpty() || !stack2.isEmpty()){
int num1 = 0;
int num2 = 0;
if(!stack1.isEmpty()){
num1 = stack1.pop();
}
if(!stack2.isEmpty()){
num2 = stack2.pop();
}
int sum = num1 + num2 + carry;
ans.push(sum % 10);
carry = sum / 10;
}
// 这里最后判断一下进位是否为0,若进位不是0则需要处理
if(carry != 0){
ans.push(carry);
}
// 生成链表
ListNode dummy = new ListNode(-1);
cur = dummy;
while(!ans.isEmpty()){
cur.next = new ListNode(ans.pop());
cur = cur.next;
}
return dummy.next;
}
}
BM12 单链表的排序问题
题目描述
给定一个节点数为n的无序单链表,对其按升序排序。
数据范围:0<n≤100000
要求:空间复杂度 O(n),时间复杂度 O(nlogn)
题解
最简单的做法是遍历链表,将链表中每个节点的值放入数组中,对数组进行排序,之后再逐个取出元素并新建链表节点,这里注意将链表的每个节点值存入数组中空间占用更小
Java代码实现
public class Solution {
/**
*
* @param head ListNode类 the head node
* @return ListNode类
*/
public ListNode sortInList (ListNode head) {
// write code here
// 特殊情况,只有一个元素时无需排序(题目中说节点数n > 0)
if(head.next == null){
return head;
}
ListNode dummy = new ListNode(-1);
ArrayList<Integer> list = new ArrayList<>();
ListNode cur = head;
while(cur != null){
list.add(cur.val);
cur = cur.next;
}
// 排序
Collections.sort(list);
// 记得把指针回退
cur = dummy;
// 逐一构造
for(Integer num : list){
cur.next = new ListNode(num);
cur = cur.next;
}
return dummy.next;
}
}
BM13 判断一个链表是否是回文结构
题目描述
给定一个链表,请判断该链表是否为回文结构。
回文是指该字符串正序逆序完全一致。
数据范围: 链表节点数 0≤n≤105,链表中每个节点的值满足 ∣val∣≤107
题解
很多实惠,直接对链表进行操作不方便,可以借助数组辅助操作,这里只要将节点中的每个数放入数组中进行比较即可,但这里注意一点,题目中说了回文但是每个节点的值满足|val|<=10^7,那{1, 235, 235, 1}算回文还是{1, 235, 532, 1}算回文,若是第一种的话,只要将每个节点的值放入数组中,两个指针同时移动,判断是否相等即可,若是另一种,则建议将整数转化为字符串进行操作,之后对于字符串中的每个字符进行操作。
Java代码实现
public class Solution {
/**
*
* @param head ListNode类 the head
* @return bool布尔型
*/
public boolean isPail (ListNode head) {
// write code here
// 特殊情况head为null
if(head == null){
return true;
}
ArrayList<Integer> nums = new ArrayList<>();
ListNode cur = head;
while(cur != null){
nums.add(cur.val);
cur = cur.next;
}
int n = nums.size();
for(int i = 0; i < n; ++i){
// 注意这里用的是equals而不是==,用==比较的是地址,只有当两个数都在常量池中时才会成功
if(!nums.get(i).equals(nums.get(n - i - 1))){
return false;
}
}
return true;
}
}
上面的代码最终是正确的,因此这里所说的回文是第一种,不过需要注意的是在比较Integer的过程中用equals而不是用==,或者就利用自动装箱机制转换为int,如:
int num1 = nums.get(i);
int num2 = nums.get(n - i - 1);
if(num1 != num2){
return false;
}
BM14 链表的奇偶重拍
题目描述
给定一个单链表,请设定一个函数,将链表的奇数位节点和偶数位节点分别放在一起,重排后输出。
注意是节点的编号而非节点的数值。
数据范围:节点数量满足 0≤n≤105,节点中的值都满足 0≤val≤1000
要求:空间复杂度 O(n),时间复杂度 O(n)
题解
新建两个链表,一个用于放奇数的节点,一个用于放偶数的节点,再将两个链表连接即可。
Java代码实现
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode oddEvenList (ListNode head) {
// write code here
// 特殊情况依旧优先考虑,当链表有012个节点时直接返回
if(head == null || head.next == null || head.next.next == null){
return head;
}
// 结果链表以及奇偶链表的头节点
ListNode dummy1 = new ListNode(-1);
ListNode dummy2 = new ListNode(-1);
// 当前的初始链表以及奇偶链表到达的位置
ListNode cur = head;
ListNode cur1 = dummy1;
ListNode cur2 = dummy2;
int len = 0;
while(cur != null){
++len;
// 这里是奇偶判断,只不过位运算更快,故选择位运算
if((len & 1) == 1){
cur1.next = cur;
cur1 = cur1.next;
}else{
cur2.next = cur;
cur2 = cur2.next;
}
cur = cur.next;
}
// 防止有粘连,先将链表断开
// 但思考之后发现只需要断开偶数的即可,因为奇数的后一步操作实际上已经断开了
if(cur1.next != null){
cur1.next = null;
}
if(cur2.next != null){
cur2.next = null;
}
// 将奇链表与偶链表连接,cur1即为奇链表的尾
// 这里其实已经避免了奇数的粘连
cur1.next = dummy2.next; // 注意这里偶链表真正开始的节点为dummy2.next(虚节点)
return dummy1.next;
}
}
这里有必要提一下我注释中说到的粘连,假设有链表{1, 2, 3, 4, 5, 6},当进行上述代码的操作之后得到的链表实际是这样的{1, 3, 5, 6} 和{2, 4, 6},我们需要的只有1 3 5,但6由于在原始链表中是5的next,故也放到了链表中,这对于原链表长度为偶数的时候不影响(因为后面连接的操作实际上已经把粘连部分去除了),但当原链表长度为奇数时就有影响了,假设有链表{1, 2, 3, 4, 5},操作之后得到的链表是{1, 3, 5}和{2, 4, 5},此时就无法得到正常结果
BM15 删除有序链表中重复的元素-I
题目描述
删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次 例如: 给出的链表为1→1→2,返回1→2. 给出的链表为1→1→2→3→3,返回1→2→3.
题解
去重最简单的办法是利用HashSet,每次将节点的值放入HashSet中,又考虑之后还要有序构建,因此考虑LinedHashSet,但此时空间复杂度为O(n),不合题意。
想到利用两个指针,一个记录当前位,另一个记录下一位,使用两个指针的目的是,当遇到当前元素与下一位元素相同时,不移动当前位,移动下一位直至当前位与下一位不同时,将当前位与下一位连接,如此遍历数组即可
Java代码实现
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
// write code here
// 特殊情况优先考虑
if(head == null || head.next == null){
return head;
}
ListNode cur = head;
ListNode nex = head.next;
// 肯定是后一个先到,但存在一种情况就是后面几个元素都相同时需要显式地令cur.nex = null
while(nex != null){
if(cur.val == nex.val){
nex = nex.next;
}else{
cur.next = nex;
cur = nex;
nex = nex.next;
}
}
// 此时cur所在的位置可能有多种
// 一是倒数第一个元素
// 二是倒数第k个元素(后面k个元素值都相同)
// 此时只需比较cur以及其后面第一个元素(前提是其后面有元素),要是相同则显式令其next为null,反之则无需操作
if(cur.next != null && cur.val == cur.next.val){
cur.next = null;
}
return head;
}
}
BM16 删除有序列表中重复的元素-II
题目描述
给出一个升序排序的链表,删除链表中的所有重复出现的元素,只保留原链表中只出现一次的元素。 例如: 给出的链表为1→2→3→3→4→4→5, 返回1→2→5. 给出的链表为1→1→1→2→3, 返回2→3.
题解
第一种思路是使用两个Set加一个数组实现一个set放全部元素,另一个set放重复的元素,数组按序放节点的值,第一次遍历,若是发现放全部元素的Set中已有这个元素,那么将此元素放入放置重复元素的Set中,此外第一次遍历还需要将链表中的全部节点值放入数组中,第二次遍历数组,若是数组中的元素并不属于放重复元素的Set中,那么以此值新建节点并连接到链表中,但此时空间复杂度为O(n),不合题意。
正确思路是:使用三个指针,pre表示前一个,cur表示当前,nex表示后一个,若是遇到相同元素,则移动nex指针直到cur与nex值不相同,若是不同则直接pre指向nex,然后三个指针依次后移一位即可
Java代码实现
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
// write code here
// 特殊情况优先考虑
if(head == null || head.next == null){
return head;
}
// 防止出现第一个元素就需要删除的情况,用虚拟头节点
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
ListNode nex = head.next;
while(nex != null){
if(cur.val == nex.val){
// 元素相同时不断移动nex指针
while(nex != null && cur.val == nex.val){
nex = nex.next;
}
//此时以及移动完了,修改指针
pre.next = nex;
cur = nex;
// 退出循环还有一种情况是nex为null了,此时不再后移nex指针
if(nex != null){
nex = nex.next;
}
}else{
// 元素不同时先连接pre和nex,再移动
pre.next = cur;
pre = cur;
cur = nex;
nex = nex.next;
}
}
return dummy.next;
}
}