上一篇https://blog.csdn.net/MrTreeson/article/details/84890660由于篇幅有限已经解决了两道链表相关的题目,这一篇继续记录一些链表相关算法题的解题方法。注:部分解法来自leetcode讨论区高亮的答案。
这题是leetcode上第142题:https://leetcode.com/problems/linked-list-cycle-ii/
题目描述:
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
直接遍历整个数组,一直访问到链表的结尾,如果结点指向null,说明没有环,时间复杂度为O(n)。
但是这种方法比较低效,而且当环存在时,会一直在环内循环,永远得不到输出结果,造成死循环,所以这种方法不可取。
用set集合存储所访问过的结点,set里面是不包含重复元素的,每访问下一个结点,就将它与set中的元素比较,如果已存在则说明有环,并返回这个结点。如果结点指向null了,就说明没有环,返回null。其时间复杂度也为O(n)代码如下:
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null)
return null;
Set set = new HashSet<>();
while(head != null) {
if(head.next == null)
return null;
if(set.contains(head))
return head;
set.add(head);
head = head.next;
}
return null;
}
定义两个指针,一个快指针fast,一个慢指针slow,快指针每次走两步,慢指针每次走一步,如果没有环,那么快指针会首先为null或者指向null。如果有环,那么在一个环中,两个速度不一致的指针必定会发生相遇,即fast == slow。
判断存在环之后,由于快慢指针相遇前在环中遍历的次数不一定,也不能保证相遇时所在结点就是环的入口,所以还需要额外的判断来找到环的入口:
代码如下:
public ListNode detectCycle1(ListNode head) {
if(head == null || head.next == 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) {
ListNode first = head;
while(first != slow) {
first = first.next;
slow = slow.next;
}
return first;
}
}
return null;
}
这是leetcode上第21题:https://leetcode.com/problems/merge-two-sorted-lists/
题目描述:
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
最简单粗暴的方法就是对每个链表的当前结点进行比较,再定义一个临时结点指向这两个结点比较后的结果,再比较下一个结点,直到链尾。当其中一个结点为空,说明另一个结点所有剩下的结点都比这个结点要大,此时再把这个临时结点指向剩下的结点即可。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null)
return l1 == null?l2:l1;
ListNode result = new ListNode(0);
ListNode prev = result;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
if (l1 != null) {
prev.next = l1;
}
if (l2 != null) {
prev.next = l2;
}
return result.next;
}
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null)
return l1 == null?l2:l1;
if(l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
假设除了l1、l2外其它的结点都已经完成合并了,那在比较之后,只需把小的结点指向已经完成合并的链表的头结点即可。对于该头结点而言,也是如此,假设它后面的结点都已经完成了合并,那么只需把它指向已完成的链表的头结点,这就是递归的逻辑。递归的终止条件就是l1或l2为null,返回另一个不为null的值。
这是leetcode第234题:https://leetcode.com/problems/palindrome-linked-list/
题目描述:
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
不管是判断回文链表还是回文字符串,用栈都是解决这些问题的好方法。把前面一半的链表存入栈中,由于栈是先进后出,所以可以利用弹栈来与head指向的后一半的结点做对比。
需要注意的是,由于链表结点数为奇数和偶数时需要入栈的个数不一样,所以要进行区别处理。
代码如下:
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null)
return true;
ListNode fast = head;
//结点个数为2时直接判断
if(fast.next.next == null){
if(head.val != head.next.val)
return false;
return true;
}
Stack stack = new Stack<>();
//利用了快慢指针的思想,当快指针指到最后时head为中点
while(fast !=null && fast.next != null) {
fast = fast.next.next;
stack.push(head.val);
head = head.next;
}
//当结点数为奇数需要把head置为下一个,因为中间结点不需要入栈也不需要判断
if(fast != null && fast.next == null)
head = head.next;
//判断
while(!stack.isEmpty()) {
if(stack.pop() != head.val)
return false;
head = head.next;
}
return true;
}
与上面类似也使用到快慢指针,但是不使用栈,而是在慢指针走的过程中对所走链表进行反转,当慢指针到达中点后,再从中间开始往左右遍历比较,不相等则返回false。代码如下:
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) return true;
ListNode fast = head;
ListNode newHead = null;
while (fast != null) {
//当结点数为奇数需要把head置为下一个,因为中间结点不需要比较
if (fast.next == null) {
head = head.next;
break;
}
else {
fast = fast.next.next;
}
//反转链表
ListNode next = head.next;
head.next = newHead;
newHead = head;
head = next;
}
//判断
while (newHead != null) {
if (newHead.val != head.val) return false;
newHead = newHead.next;
head = head.next;
}
return true;
}
这是leetcode上第25题:https://leetcode.com/problems/reverse-nodes-in-k-group/
题目描述:
给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。
示例 :
给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
说明 :
这道题跟前面反转链表的题目类似,但是比它更难一点,因为需要划分到每k个结点进行反转。
跟前面一样,也可以用递归来解决这个问题,不同的是需要对链表进行分组,每组进行递归,然后在组内完成链表的反转,再返回该组反转后的头结点。所以,除了递归和反转的步骤之外,还需要有专门进行k分组的模块,代码如下:
public ListNode reverseKGroup(ListNode head, int k) {
if(head == null || head.next == null)
return head;
ListNode curr = head;
int count = 0;
while(curr !=null && count != k) {
curr = curr.next; //得到第k+1个结点
count++;
}
if(count == k) {
curr = reverseKGroup(curr, k);
while(count > 0) {
ListNode temp = head.next;
head.next = curr;
curr = head;
head = temp;
count--;
}
head = curr;
}
return head;
}
这段代码跟前面反转链表的代码其实没什么区别,只是每次递归之后都要重新进行一次k分组,在这个分组里面进行反转的操作。要注意的是,这里的count起始值为0,得到的是curr等于第k+1个结点,因为这样curr才能代表已完成反转的该组链表的头结点,然后用 head.next = curr; 使得当前组的头结点指向该反转后的链表,然后再进行组内后面的反转操作。
使用if(count == k)来判断,当不满足条件也就是剩下的结点数小于k,就可以直接忽略,返回剩下链表的头结点,就可以使这些部分保持原顺序。
不使用递归的时候,就需要从头结点开始,首先进行k分组,并在分组内完成反转,这里专门将反转作为一个方法,并返回完成反转后的结尾,该结尾指向下一组要反转的链表。
public ListNode reverseKGroup(ListNode head, int k) {
ListNode begin;
if (head==null || head.next ==null || k==1)
return head;
ListNode dummyhead = new ListNode(-1);
dummyhead.next = head;
begin = dummyhead;
int i=0;
while (head != null){
i++;
if (i%k == 0){ //当head移动到第k个元素时开始反转
begin = reverse(begin, head.next);
head = begin.next;
} else {
head = head.next;
}
}
return dummyhead.next;
}
public ListNode reverse(ListNode begin, ListNode end){
ListNode curr = begin.next;
ListNode next, first;
ListNode prev = begin;
first = curr;
while (curr!=end){
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
begin.next = prev;
first.next = curr;
return first;
}