0%

链表问题算法(二)

1,打印两个有序链表中的公共部分
题目:给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
解答思路:
1,如果head1的值小于head2,则head1往下移动
2,如果head2的值小于head1,则head2往下移动
3,如果head1的值与head2的值相等,则打印这个值,然后head1与head2都往下移
4,head1或head2有任何一个移动到null,整个过程停止
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void printCommonPart(Node head1, Node head2) {
System.out.print("Common Part: ");
while (head1 != null && head2 != null) {
if (head1.value < head2.value) {
head1 = head1.next;
} else if (head1.value > head2.value) {
head2 = head2.next;
} else {
System.out.print(head1.value + " ");
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}

2,在单链表和双链表中删除倒数第K个数
题目:分别实现两个函数,一个可以删除单链表倒数第K个数,另一个可以删除双链表中倒数第K个节点。
要求:如果链表长度为N,要求时间复杂度达到O(N),额外空间复杂度O(1)。
先看单链表的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   public static class Node {
public int value;
public Node next;

public Node(int data) {
this.value = data;
}
}

public static Node removeLastKthNode(Node head, int lastKth) {
if (head == null || lastKth < 1) {
return head;
}
Node cur = head;
while (cur != null) {
lastKth--;
cur = cur.next;
}
if (lastKth == 0) {
head = head.next;
}
if (lastKth < 0) {
cur = head;
while (++lastKth != 0) {
cur = cur.next;
}
cur.next = cur.next.next;
}
return head;
}

双链表调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
   public static class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;

public DoubleNode(int data) {
this.value = data;
}
}

public static DoubleNode removeLastKthNode(DoubleNode head, int lastKth) {
if (head == null || lastKth < 1) {
return head;
}
DoubleNode cur = head;
while (cur != null) {
lastKth--;
cur = cur.next;
}
if (lastKth == 0) {
head = head.next;
head.last = null;
}
if (lastKth < 0) {
cur = head;
while (++lastKth != 0) {
cur = cur.next;
}
DoubleNode newNext = cur.next.next;
cur.next = newNext;
if (newNext != null) {
newNext.last = cur;
}
}
return head;
}

3,删除链表的中间节点和a/b处的节点
题目:给定链表头节点head,实现删除链表的中间节点的函数。
例如:
1–>2:删除1节点;
1–>2–>3:删除2节点;
1–>2–>3–>4:删除2节点;
1–>2–>3–>4–>5:删除3节点;
进阶:给定链表头节点head、整数a和b,实现删除位于a/b处节点的函数。
例如:
链表:1–>2–>3–>4–>5,假设a/b的值为r。
如果r等于0,不删除任何节点;
如果r在区间(0,1/5]上,删除节点1;
如果r在区间(1/5,2/5]上,删除节点2;
如果r在区间(2/5,3/5]上,删除节点3;
如果r在区间(3/5,4/5]上,删除节点4;
如果r在区间(4/5,1/5]上,删除节点5;
删除中间节点的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   public static Node removeMidNode(Node head) {
if (head == null || head.next == null) {
return head;
}
if (head.next.next == null) {
return head.next;
}
Node pre = head;
Node cur = head.next.next;
while (cur.next != null && cur.next.next != null) {
pre = pre.next;
cur = cur.next.next;
}
pre.next = pre.next.next;
return head;
}

进阶问题,删除a/b处的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   public static Node removeByRatio(Node head, int a, int b) {
if (a < 1 || a > b) {`
return head;
}
int n = 0;
Node cur = head;
while (cur != null) {
n++;
cur = cur.next;
}
n = (int) Math.ceil(((double) (a * n)) / (double) b);
if (n == 1) {
head = head.next;
}
if (n > 1) {
cur = head;
while (--n != 1) {
cur = cur.next;
}
cur.next = cur.next.next;
}
return head;
}

4,反转单向链表和双向链表
题目:分别实现反转单向链表和双向链表的函数
要求:长度为n,时间复杂度为O(n),额外空间复杂度要求为O(1)。
反转单链表代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
  public static Node reverseList(Node head) {
Node pre = null;
Node next = null;
while (head != null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
```
反转双向链表代码:

public static DoubleNode reverseList(DoubleNode head) {
    DoubleNode pre = null;
    DoubleNode next = null;
    while (head != null) {
        next = head.next;
        head.next = pre;
        head.last = next;
        pre = head;
        head = next;
    }
    return pre;
}
1
2
3
4
5
6
7
8
9
5,反转部分单向链表
题目:给定一个单向链表的头节点,以及两个整数from和to,在单向链表上把第from个节点到第to个节点这一部分进行反转。
例如:1-->2-->3-->4-->5-->null,from=2,to=4.
调整结果为:1-->4-->3-->2-->5-->null
再如:1-->2-->3-->null,from=1,to=3
调整结果为:3-->2-->1-->null
要求:
1,链表长度为n,时间复杂度为O(n),额外空间复杂度要求为O(1)。
2,如果不满足1<=frim<=to<==n,则不调整。
public static Node reversePart(Node head, int from, int to) { int len = 0; Node node1 = head; Node fPre = null; Node tPos = null; while (node1 != null) { len++; fPre = len == from - 1 ? node1 : fPre; tPos = len == to + 1 ? node1 : tPos; node1 = node1.next; } if (from > to || from < 1 || to > len) { return head; } node1 = fPre == null ? head : fPre.next; Node node2 = node1.next; node1.next = tPos; Node next = null; while (node2 != tPos) { next = node2.next; node2.next = node1; node1 = node2; node2 = next; } if (fPre != null) { fPre.next = node1; return head; } return node1; }
1
2
3
4
5

6,环形单链表的约瑟夫问题
题目:39个犹太人与Josephus以及他的朋友躲到一个洞中,39个犹太人宁愿死也不愿意被敌人抓到,于是决定了一个自杀方式,41个人排成一个圈,由第一个人开始报数,报道3的人开始自杀,然后下一个人继续重新报1,报到3的人再自杀,这样再一次下去,直到剩下最后一个人时,那个人可以自由选择自己的命运。这就是著名的约瑟夫问题,现在请用单向环形链表描述该结构呈现整个自杀过程。
思路:
代码:
public static Node josephusKill1(Node head, int m) { if (head == null || head.next == head || m < 1) { return head; } Node last = head; while (last.next != head) { last = last.next; } int count = 0; while (head != last) { if (++count == m) { last.next = head.next; count = 0; } else { last = last.next; } head = last.next; } return head; }
1
2
3
4
5
6
   以上代码的时间复杂度是O(n*m),显然不符合进阶的要求。
进阶的代码:
7,判断链表是否为回文结构
题目:判断是否是回文结构,例如:1-->2-->1,返回true;1-->2-->3,返回false。
思路:利用栈即可,遍历栈,遍历过程中把节点压入栈,因为栈是先进后出的,所以在遍历后,从栈顶到栈底的节点值出现顺序会与原链表从左到右的值顺序反过来,那么,如果一个链表是回稳结构,如果出现的值还是一样的,那就是回文结构,否则不是。
代码如下:
// need n extra space。空间复杂度为O(n/2)。利用栈 public static boolean isPalindrome1(Node head) { Stack<Node> stack = new Stack<Node>(); Node cur = head; while (cur != null) { stack.push(cur); cur = cur.next; } while (head != null) { if (head.value != stack.pop().value) { return false; } head = head.next; } return true; }
1
需要n/2的额外空间复杂度的代码:
// need n/2 extra space,额外空间复杂度为O(n/2)。这个方法是对第一种方法进行了改进,每次压右半部分的节点,这样从栈顶到栈底的顺序就相当于反过来了,然后与前半部分一一比较。 public static boolean isPalindrome2(Node head) { if (head == null || head.next == null) { return true; } Node right = head.next; Node cur = head; while (cur.next != null && cur.next.next != null) { right = right.next; cur = cur.next.next; } Stack<Node> stack = new Stack<Node>(); while (right != null) { stack.push(right); right = right.next; } while (!stack.isEmpty()) { if (head.value != stack.pop().value) { return false; } head = head.next; } return true; }
1
空间复杂度为仅仅为O(1):
// need O(1) extra space 方法三:不需要栈和其他数据结构,只用有限几个变量,其额外空间复杂度为O(1),可以在O(n)内完成所有的过程,也就是满足进阶的要求。具体过程如下: 1,首先改变链表右半部分的结构,使整个右半区反转,最后指向中间节点。 例如:1-->2-->3-->2-->1,通过这一步将其调整之后结构如下:1-->2-->3<--2<--1,链表1-->2-->3-->3-->2-->1,调整后为:1-->2-->3<--3<--2<--1。 我们把左半边区域第一个节点记为leftStart,把反转后的右半部分的最右边的节点记为rightStart。 2,leftStart和rightStart同时向中间点移动,移动每一步都比较leftStart和rightStart节点的值,看是否一样,一样的话,说明链表为为回文结构,否则不是回文结构。 3,不管最后返回时false还是true,在返回前都应该把链表恢复成原来的状况。 4,链表恢复成原来的样子,返回检查结果。以下是代码: public static boolean isPalindrome3(Node head) { if (head == null || head.next == null) { return true; } Node n1 = head; Node n2 = head; while (n2.next != null && n2.next.next != null) { // find mid node,找到中间节点,和方法二有一点不一样,方法二的中间是从head.next开始,方法三是从head开始,这有什么区别呢?在节点为偶数的时候,方法二得到的中间是中间两个节点的后一个节点,而方法三得到的中间节点得到的是中间两个节点的前一个节点,为什么?因为方法二的情况下要压入节点进栈就是从后半部分开始,这样就可以理解了,实际上理解为后半部分更恰当。 n1 = n1.next; // n1 -> mid n2 = n2.next.next; // n2 -> end } n2 = n1.next; // n2 -> right part first node n1.next = null; // mid.next -> null Node n3 = null; while (n2 != null) { // right part convert n3 = n2.next; // n3 -> save next node n2.next = n1; // next of right node convert n1 = n2; // n1 move n2 = n3; // n2 move } n3 = n1; // n3 -> save last node n2 = head;// n2 -> left first node boolean res = true; while (n1 != null && n2 != null) { // check palindrome if (n1.value != n2.value) { res = false; break; } n1 = n1.next; // left to mid n2 = n2.next; // right to mid } n1 = n3.next; n3.next = null; while (n1 != null) { // recover list n2 = n1.next; n1.next = n3; n3 = n1; n1 = n2; } return res; } ```