本文将介绍:
-
单链表反转
-
求中间节点
- 删除倒数第K个节点
- 合并两个有序链表
- 检测环,并找到入口点
- 两个单链表相交的起始节点
- 奇偶链表
- 回文链表
Node定义:
public static class Node {
private int data;
private Node next;
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
public int getData() {
return data;
}
}
1、单链表反转
原文(C++语言实现):https://blog.csdn.net/feliciafay/article/details/6841115
方法:使用3个指针遍历单链表,逐个链接点进行反转。
使用p和q两个指针配合工作,使得两个节点间的指向反向,同时用r记录剩下的链表。
p = head;
q = head.next;
head.next = null;
现在进入循环体,这是第一次循环。
r = q.next;
q.next = p;
p = q;
q =r;
第二次循环。
r = q.next
q.next = p;
p = q;
q = r
第三次循环。。。。。
具体代码如下
static Node reverseCsdn(Node head){
//少于两个节点没有必要反转
if(head == null || head.next == null){
return head;
}
Node p,q,r;
p = head;
q = head.next;
head.next = null; //旧的头指针就是新的尾指针,next需要指向null
while (q != null){
r = q.next;//先保留下一个step要处理的指针
q.next = p;//然后pq交替工作进行反向
p = q;
q = r;
}
head = p;//最后q必然指向null,所以返回了p作为新的头指针
return head;
}
2、求中间节点
使用快慢指针,快指针走到尾节点时,慢指针恰好在链表中间。
// 求中间结点
public static Node findMiddleNode(Node list) {
if (list == null) return null;
Node fast = list;
Node slow = list;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
3、删除倒数第K个节点
为了能够只遍历一次就能找到倒数第k个节点,可以定义两个指针:
(1)第一个指针从链表的头指针开始遍历向前走k-1,第二个指针保持不动;
(2)从第k步开始,第二个指针也开始从链表的头指针开始遍历;
(3)由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾结点时,第二个指针(走在后面的)指针正好是倒数第k个结点。
下图展示了在有6个结点的链表上找倒数第3个结点的过程:
代码如下:
// 删除倒数第K个结点
public static Node deleteLastKth(Node list, int k) {
Node fast = list;
int i = 1;
while (fast != null && i < k) {
fast = fast.next;
++i;
}
if (fast == null) return list;
Node slow = list;
Node prev = null;
while (fast.next != null) {
fast = fast.next;
prev = slow;
slow = slow.next;
}
if (prev == null) {
list = list.next;
} else {
prev.next = prev.next.next;
}
return list;
}
4、合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
//合并两个有序链表
public static Node mergeTwoLists(Node la, Node lb){
if(la == null) return lb;
if(lb == null) return la;
Node head = null;
if(la.data <= lb.data){
head = la;
head.next = mergeTwoLists(la.next, lb);
} else {
head = lb;
head.next = mergeTwoLists(la, lb.next);
}
return head;
}
5、检测环,并找到入口点
(1)判断是否有环:
对于这个问题我们可以采用“快慢指针”的方法。就是有两个指针fast和slow,开始的时候两个指针都指向链表头head,然后在每一步操作中slow向前走一步即:slow = slow->next,而fast每一步向前两步即:fast = fast->next->next。
由于fast要比slow移动的快,如果有环,fast一定会先进入环,而slow后进入环。当两个指针都进入环之后,经过一定步的操作之后二者一定能够在环上相遇,并且此时slow还没有绕环一圈,也就是说一定是在slow走完第一圈之前相遇。
slow每次向前走一步,fast向前追了两步,因此每一步操作后fast到slow的距离缩短了1步,这样继续下去就会使得
两者之间的距离逐渐缩小:...、5、4、3、2、1、0 -> 相遇。又因为在同一个环中fast和slow之间的距离不会大于换的长度,因此
到二者相遇的时候slow一定还没有走完一周(或者正好走完以后,这种情况出现在开始的时候fast和slow都在环的入口处)。
(2)找入口点
思路:
如果单链表有环,当slow指针和fast指针相遇时,slow指针还没有遍历完链表,而fast指针已经在环内循环n(n>=1)圈了,假设此时slow指针走了s步,fast指针走了2s步,r为fast在环内转了一圈的步数,a为从链表头到入口点的步数,b为从入口点到相遇点的步数,c为从相遇点再走c步到达入口点,L为整个链表的长度。
slow指针走的步数:
s = a + b
fast指针走的步数:
2s = s + n*r 即:s = n*r
链表的长度:
L = a + b + c = a+r
由上可得:
a + b = n*r = (n - 1)*r + r
而r = L - a,所以:
a + b = (n - 1)*r + L - a
a = (n - 1)*r + L - a - b
而L - a - b = c,所以:
a = (n -1)*r +c
综上可得:从链表头到环入口点等于(n - 1)循环内环 + 相遇点到环入口点,于是在链表头和相遇点分别设置一个指针,同时出发,每次各走一步,它们必定会相遇,且第一次相遇的点就是环入口点。
//找到环入口点
public static Node findLoopPoint(Node list) {
if (list == null) return null;
Node fast = list,slow = list;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (slow == fast) break;
}
if(fast == null || fast.next == null){
return null;
}
//如果有环,slow指向连表头,此时fast指向相遇点
slow = list;
while (slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
6、找到两个单链表相交的起始节点
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
因为链表是单向链表,所以相交链表的形状就像倒置的 Y,这就有个==特点==:相交点之后的结点都相同。所以找到第一个相同的结点就是链表的第一个公共结点。
如果正向遍历链表,因为链表的长度不相同,所以无法通过的时间使他们的步伐统一。解决办法是首先比较链表两个的长度,然后让较长的链表的先走他们长度的差值,然后同一步伐一起走,知道走到的结点相同时,就找的了第一个公共结点。
时间复杂度分析:空间复杂度O(1),时间复杂度O(n)
//计算链表的长度
private int calculateNodes(Node head){
int val=0;
for(;head!=null;head=head.next)
val++;
return val;
}
//循环遍历,需要借助于栈
public ListNode getIntersectionNode(Node headA, Node headB) {
if(headA==null || headB==null)
return null;
//用来保存链表之间的差值
int temp=0;
ListNode res=null;
//计算两个链表的长度,较长的链表先走几步
int lenA=calculateNodes(headA);
int lenB=calculateNodes(headB);
if(lenA>lenB){
temp=lenA-lenB;
for(int i=0;i<temp;i++)
headA=headA.next;
}
else{
temp=lenB-lenA;
for(int i=0;i<temp;i++)
headB=headB.next;
}
//两个链表同步走,当脚下节点相同时,就找到了
for(;headA!=null && headB!=null;headA=headA.next,headB=headB.next){
if(headA==headB){
res=headA;
break;
}
}
return res;
}
7、奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:
- 应当保持奇数节点和偶数节点的相对顺序。
- 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
解题:
我的想法是这样的,将原链表拆分成奇偶链表然后在合并。
这样的话涉及几个步骤:
1.两个链表的初始节点,如STEP0所示。
2.odd跳到even的下一个,even再跳到odd的下一个,然后将even串到evenHead上,如此往复(请看STEP1、STEP2)。
3.最后,将odd的下一个串到evenHead,实现链接。
class Solution {
public ListNode oddEvenList(ListNode head) {
if(head==null)
return null;
ListNode oddList=head;
ListNode evenList=head.next;
ListNode evenHead = evenList;
while(evenList!=null&&evenList.next!=null)
{
oddList.next=evenList.next;
oddList=oddList.next;
evenList.next=oddList.next;
evenList=evenList.next;
}
oddList.next=evenHead;
return head;
}
}
8、回文链表
请编写一个函数,检查链表是否为回文。
给定一个链表ListNode pHead,请返回一个bool,代表链表是否为回文。
public class Palindrome {
public boolean isPalindrome(ListNode pHead){
ListNode fast = pHead;
ListNode slow = pHead;
Stack<Integer> stack = new Stack<Integer>();
/**
* 将链表的前半部分元素装入栈中,当快速runner
*(移动的速度是慢速runner的两倍)
* 到底链表尾部时,则慢速runner已经处于链表中间位置
*/
while(fast != null && fast.next != null){
stack.push(slow.val);
slow = slow.next;
fast = fast.next.next;
}
//当链表为奇数个时,跳过中间元素
if (fast != null) {
slow = slow.next;
}
while(slow != null){
//如果两者不相同,则该链表不是回文串
if (stack.pop() != slow.val) {
return false;
}
slow = slow.next;
}
return true;
}
}