目录
说明:题目来源于牛客网
1.反转链表
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
public ListNode ReverseList(ListNode head) {
if(head == null) {
return null;
}
ListNode prev = null;
ListNode cur = head;
while( cur!= null){
ListNode curNext= cur.next;
cur.next = prev;
prev = cur;
cur = curNext;
}
return prev;
}
时间复杂度:O(n), 遍历一次链表
空间复杂度:O(1)
2. 链表内指定区间反转
将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n),空间复杂度 O(1)。
例如:
给出的链表为 1→2→3→4→5→NULL, m=2,n=4
返回 1→4→3→2→5→NULL.。
思路
- 1:我们可以在链表前加一个表头,后续返回时去掉就好了,因为如果要从链表头的位置开始反转,在多了一个表头的情况下就能保证第一个节点永远不会反转,不会到后面去。
- 2:使用两个指针,一个指向当前节点,一个指向前序节点。
- 3:依次遍历链表,到第m个的位置。
- 4:对于从m到n这些个位置的节点,依次断掉指向后续的指针,反转指针方向。
- 5:返回时去掉我们添加的表头。
public ListNode reverseBetween (ListNode head, int m, int n) {
// write code here
if(head == null) {
return null;
}
//设置一个新的链表头
ListNode newHead = new ListNode(-1);
newHead.next = head;
//前序节点
ListNode node = newHead;
//当前节点
ListNode cur = head;
//放入M前面的节点
for (int i = 0; i < m-1; i++) {
node = cur;
cur = cur.next;
}
//反转m 到n
for(int i = m; i < n; i++) {
//标记好节点
ListNode tmp = cur.next;
cur.next = tmp.next;
tmp.next = node.next;
node.next = tmp;
}
return newHead.next;
}
复杂度:
- 时间复杂度:O(n),最坏情况下需要遍历全部链表节点,比如m为链表最后一个位置,或者n为链表最后一个位置时
- 空间复杂度:O(1),常数级指针变量,无额外辅助空间使用
3. 链表中的节点每k个一组翻转
将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表。如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样。你不能更改节点中的值,只能更改节点本身。
思路:
- 1:每次从进入函数的头节点优先遍历链表k次,分出一组,若是后续不足k个节点,不用反转直接返回头。
- 2:从进入函数的头节点开始,依次反转接下来的一组链表,反转过程同BM1.反转链表。
- 3:这一组经过反转后,原来的头变成了尾,后面接下一组的反转结果,下一组采用上述递归继续。
public ListNode reverseKGroup (ListNode head, int k) {
// write code here
//连接K前面的节点
ListNode tail = head;
for(int i = 0; i < k; i++) {
if (tail == null) return head;
tail = tail.next;
}
//到这一步,tail就走到了下一组的头结点
//到达K,翻转
ListNode pre = null;//遍历的前序节点
ListNode cur = head;//原本的头不动,用一个新的来记录
while(cur != tail) {
ListNode curNext = cur.next;
cur.next = pre;
pre = cur;
cur = curNext;
}
//到这里就翻转完一组K
//递归下一组
//原本的头节点因为不动,还在原来的位置,现在成了尾结点
//尾结点连接下一组的头节点
head.next = reverseKGroup(tail,k);//此处的tail就是每一组的头结点
return pre;
}
复杂度分析:
- 时间复杂度:O(n),一共遍历链表n个节点
- 空间复杂度:O(n),递归栈最大深度为n/k
4. 合并两个排序的链表
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
思路:
- 如果l1指向的结点值小于等于l2指向的结点值,则将l1指向的结点值链接到cur的next指针,然后l1指向下一个结点值
- 否则,让l2指向下一个结点值
- 循环步骤1,2,直到l1或者l2为nullptr
- 将l1或者l2剩下的部分链接到cur的后面
public ListNode Merge(ListNode list1,ListNode list2) {
if (list1 == null && list2 == null) {
return null;
}
//建一个新的链表,将合并后的放入其中
ListNode newHead = new ListNode(-1);
ListNode tmp = newHead;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
//放入一个节点
tmp.next = list1;
list1 = list1.next;
tmp = tmp.next;
}else {
tmp.next = list2;
list2 = list2.next;
tmp = tmp.next;
}
}
//到这走完其中一个链表了
if(list1 != null) {
tmp.next = list1;//因为是有序了,所以连接好前面一个节点就写
}
if (list2 != null) {
tmp.next = list2;
}
return newHead.next;
}
}
时间复杂度:O(m+n)
空间复杂度:O(m+n),每一次递归,递归栈都会保存一个变量,最差情况会保存(m+n)个变量
5.判断链表中是否有环
判断给定的链表中是否有环。如果有环则返回true,否则返回false。
思路
- 1:设置快慢两个指针,初始都指向链表头。
- 2:遍历链表,快指针每次走两步,慢指针每次走一步。
- 3:如果快指针到了链表末尾,说明没有环,因为它每次走两步,所以要验证连续两步是否为NULL。
- 4:如果链表有环,那快慢双指针会在环内循环,因为快指针每次走两步,因此快指针会在环内追到慢指针,二者相遇就代表有环。
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;
if(fast == slow) {
return true;
}
}
return false;
}
- 时间复杂度:O(n),最坏情况下遍历链表n个节点
- 空间复杂度:O(1),仅使用了两个指针,没有额外辅助空间
6.链表中环的入口
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
思路;
- 1:判断链表是否有环,并找到相遇的节点。
- 2:慢指针继续在相遇节点,快指针回到链表头,两个指针同步逐个元素逐个元素开始遍历链表。
- 3:再次相遇的地方就是环的入口。
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;
}
}
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;
}
7. 链表中倒数最后K个节点
输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
思路:
- 1:准备一个快指针,从链表头开始,在链表上先走k步。
- 2:准备慢指针指向原始链表头,代表当前元素,则慢指针与快指针之间的距离一直都是k。
- 3:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数k个元素的位置。
public ListNode FindKthToTail (ListNode pHead, int k) {
ListNode fast = pHead;
ListNode slow = pHead;
//快指针先行k步
for (int i = 0; i < k; i++) {
if (fast != null) {
fast = fast.next;
} else {
//达不到k步说明链表过短,没有倒数k
return null;
}
}
//快慢指针同步,快指针先到底,慢指针指向倒数第k个
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
- 时间复杂度:O(n),总共遍历nnn个链表元素
- 空间复杂度:O(1),常数级指针变量,无额外辅助空间使用
8. 删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第 n 个节点并返回链表的头指针
思路:
- 1:给链表添加一个表头,处理删掉第一个元素时比较方便。
- 2:准备一个快指针,在链表上先走n步。
- 3:准备慢指针指向原始链表头,代表当前元素,前序节点指向添加的表头,这样两个指针之间相距就是一直都是n。
- 4:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数n个元素的位置。
- 5:最后将该节点前序节点的指针指向该节点后一个节点,删掉这个节点。
public ListNode removeNthFromEnd (ListNode head, int n) {
// write code here
ListNode fast = head;
ListNode slow = head;
ListNode pre = new ListNode(-1);
pre.next = head;
ListNode cur = pre;
while(n != 0) {
fast = fast.next;
n--;
}
while(fast != null) {
fast = fast.next;
cur = slow;
slow = slow.next;
}
cur.next = slow.next;
return pre.next;
}
9. 两个链表的第一个公共节点
思路:
1. 利用双指针法,先算出两个链表的长度
2.然后走两个链表的长度差值
在一起走,相遇就是一第一个公共节点
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null && pHead2 == null) {
return null;
}
ListNode pl = pHead1;
ListNode ps = pHead2;
int len1 = 0;
int len2 = 0;
//1.遍历两个链表,求长度
while(pl != null) {
len1++;
pl = pl.next;
}
//走完再回到头节点
pl = pHead1;
while (ps != null) {
len2++;
ps = ps.next;
}
ps = pHead2;
//差值
int len = len1 - len2;
//pl始终是记录最长链表的
//len小于0 ,说明pHead2长
if (len < 0) {
pl = pHead2;
ps = pHead1;
len = len2 - len1;
}
//标记好最长的之后,长的开始走差值
while (len > 0) {
pl = pl.next;
len--;
}
//到这里,剩下的节点数相同,
//同时走,相遇就是第一个公共节点
while (ps != pl) {
ps = ps.next;
pl = pl.next;
}
return ps;
}
10. 链表相加
假设链表中每一个节点的值都在 0 - 9 之间,那么链表整体就可以代表一个整数。
给定两个这种链表,请生成代表两个整数相加值的结果链表。
思路:
- 1:任意一个链表为空,返回另一个链表就行了,因为链表为空相当于0,0加任何数为0,包括另一个加数为0的情况。
- 2:相继反转两个待相加的链表
- 3:设置返回链表的链表头,设置进位carry=0.
- 4:从头开始遍历两个链表,直到两个链表节点都为空且carry也不为1. 每次取出不为空的链表节点值,为空就设置为0,将两个数字与carry相加,然后查看是否进位,将进位后的结果(对10取模)加入新的链表节点,连接在返回链表后面,并继续往后遍历。
- 5:返回前将结果链表再反转回来。
public ListNode addInList (ListNode head1, ListNode head2) {
// write code here
//任意链表为空,返回另一个
if (head1 == null) return head2;
if (head2 == null) return head1;
//反转两个链表
head1 = ReverseList(head1);
head2 = ReverseList(head2);
//添加表头
ListNode res = new ListNode(-1);
ListNode head = res;
//进位符号
int carry = 0;
//只要某个链表还有或者进位还有
while (head1 != null || head2 != null || carry != 0) {
//链表不为空则取其值
int val1 = head1 == null ? 0 : head1.val;
int val2 = head2 == null ? 0 : head2.val;
//相加
int temp = val1 + val2 + carry;
//获取进位
carry = temp / 10;
temp %= 10;
//添加元素
head.next = new ListNode(temp);
head = head.next;
//移动下一个
if (head1 != null) head1 = head1.next;
if (head2 != null) head2 = head2.next;
}
//结果反转回来
return ReverseList(res.next);
}
public ListNode ReverseList(ListNode pHead) {
if (pHead == null) return null;
ListNode cur = pHead;
ListNode pre = null;
while (cur != null) {
ListNode curNext = cur.next;
cur.next = pre;
pre = cur;
cur = curNext;
}
return pre;
}
11. 判断一个链表是否为回文结构
思路:
1.找中间节点
2.反转后半链表
3.两边往中间走,判断
public boolean isPail (ListNode head) {
// 1.找中间节点
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//2.反转后半链表
ListNode pre = null;
while(slow != null) {
ListNode node = slow.next;
slow.next = pre;
pre = slow;
slow = node;
}
//两边往中间走,判断
ListNode cur = head;
while(pre != null) {
if(cur.val != pre.val) {
return false;
}
cur = cur.next;
pre = pre.next;
}
return true;
}
12. 链表的奇偶重排
给定一个单链表,请设定一个函数,将链表的奇数位节点和偶数位节点分别放在一起,重排后输出。
注意是节点的编号而非节点的数值。
思路:
- 1:判断空链表的情况,如果链表为空,不用重排。
- 2:使用双指针odd和even分别遍历奇数节点和偶数节点,并给偶数节点链表一个头。
- 3:上述过程,每次遍历两个节点,且even在后面,因此每轮循环用even检查后两个元素是否为NULL,如果不为再进入循环进行上述连接过程。
- 4:将偶数节点头接在奇数最后一个节点后,再返回头部。
public ListNode oddEvenList (ListNode head) {
// write code here
//如果链表为空,不用重排
if (head == null) return head;
// odd 开头指向第一个节点
ListNode odd = head;
//even 指向开头第二个
ListNode even = head.next;
//指向even开头
ListNode evenHead = even;
while (even != null && even.next != null) {
//odd连接even的后一个,即奇数位
odd.next = even.next;
//odd进入后一个奇数位
odd = odd.next;
//even连接后一个奇数的后一位,即偶数位
even.next = odd.next;
even = even.next;
}
//even整体接在odd后面
odd.next = evenHead;
return head;
}
- 时间复杂度:O(n),遍历一次链表的所有节点
- 空间复杂度:O(1),常数级指针,无额外辅助空间
13. 删除有序链表中重复的元素-1
删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次
思路:
- 1:判断链表是否为空链表,空链表不处理直接返回。
- 2:使用一个指针遍历链表,如果指针当前节点与下一个节点的值相同,我们就跳过下一个节点,当前节点直接连接下个节点的后一位。
- 3:如果当前节点与下一个节点值不同,继续往后遍历。
- 4:循环过程中每次用到了两个节点值,要检查连续两个节点是否为空。
public ListNode deleteDuplicates (ListNode head) {
// write code here
if (head == null) return null;
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
}else {
cur = cur.next;
}
}
return head;
}
14. 删除有序链表中重复的元素-2
给出一个升序排序的链表,删除链表中的所有重复出现的元素,只保留原链表中只出现一次的元素。
思路:
- 1:给链表前加上表头,方便可能的话删除第一个节点。
- 2:遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去。
- 3:在step 2中这一连串相同的节点前的节点直接连上后续第一个不相同值的节点。
- 4:返回时去掉添加的表头。
public ListNode deleteDuplicates (ListNode head) {
// write code here
if(head == null) return null;
ListNode pre = new ListNode(-1);
pre.next = head;
ListNode cur = pre;
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 pre.next;
}
- 时间复杂度:O(n),其中nnn为链表节点数,只有一次遍历
- 空间复杂度:O(1),只开辟了临时指针,常数级空间。