目录
1)创建一个新链表。遍历原来的链表,如果不是val就插入新链表中。
3、将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(三种方法)
4、编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。注意:分割后保证原来的数据顺序不变。
5、给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
1)先求出链表的长度len,然后向后移动(len / 2)次即可。
2)定义两个结点:一个结点为fast,每次循环移动两次;另一个为slow结点,每次循环移动一次。
1)先求出链表的长度len,然后重新遍历向后移动(len - k)次即可。
8、在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
11、 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL。
12、给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝(深拷贝)。
1、反转链表
1)不断从原来链表中取出结点,头插到一个新链表上。
(1)需要一个新链表,并且是一个空链表。
Node result = null; // result:是新链表的第一个结点的引用
(2)遍历原来的链表。
Node cur = head;
while(cur != null) {
Node next = cur.next;
}
(3)把每一个遍历到的结点cur,头插到新链表result上。
① 已经有结点
② cur.next = result;
③ 更新最新的第一个结点: result = cur;
public ListNode reverseList(ListNode head) {
ListNode result = null;
ListNode cur = head;
while(cur != null) {
ListNode next = cur.next;
cur.next = result;
result = cur;
cur = next;
}
return result;
}
2)定义三个结点进行反转。
public ListNode reverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode p1 = null;
ListNode p2 = head;
ListNode p3 = head.next;
while (p2 != null) {
p2.next = p1;
p1 = p2;
p2 = p3;
if (p3 != null) {
p3 = p3.next;
}
}
return p1;
}
2、删除链表中等于给定值 val 的所有节点。
1)创建一个新链表。遍历原来的链表,如果不是val就插入新链表中。
public ListNode removeElements(ListNode head, int val) {
ListNode tmpHead = new ListNode(-1);
tmpHead.next = head;
ListNode prev = tmpHead;
ListNode cur = head;
while (cur != null) {
if (cur.val == val) {
prev.next = cur.next;
} else {
prev = cur;
}
cur = cur.next;
}
return tmpHead.next;
}
2)遍历链表,如果是val就直接删除。
public ListNode removeElements(ListNode head, int val) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
if (cur.val == val) {
if (cur == head) {
head = cur.next;
} else {
prev.next = cur.next;
}
} else {
prev = cur;
}
cur = cur.next;
}
return head;
}
优化:
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
ListNode prev = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
prev.next = cur.next;
} else {
prev = cur;
}
cur = cur.next;
}
if (head.val == val) {
head = head.next;
}
return head;
}
3、将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(三种方法)
1)创建新链表,进行尾插。
ListNode first = l1; ListNode second = l2;
ListNode result = null;
while(first != null && second != null) {
if(first.val <=second.val) {
// 把first尾插到result中
// 分情况讨论
}
if(first == null) {
// 把second尾插到result中
} else {
// 把first尾插到result中
}
}
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) {
return l2;
}
if(l2 == null) {
return l1;
}
ListNode first = l1;
ListNode second = l2;
ListNode result = null;
ListNode last = null;
while(first != null && second != null) {
if(first.val <= second.val) {
ListNode next = first.next;
if(result == null) {
result = first;
} else {
last.next = first;
}
last = first;
first = next;
} else {
ListNode next = second.next;
if(result == null) {
result = second;
} else {
last.next = second;
}
last = second;
second = next;
}
}
if(first != null) {
last.next = first;
} else {
last.next = second;
}
return result;
}
2)与方法1)类似,只是修改了while循环的条件。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode cur1 = l1;
ListNode cur2 = l2;
while (cur1 != null || cur2 != null) {
if (cur1 == null) {
// 尾插 cur2
} else if (cur2 == null) {
// 尾插 cur1
} else {
if (cur1.val <= cur2.val) {
}
}
}
}
3)递归方法。
可以如下递归地定义在两个链表里的 merge 操作(忽略边界情况,比如空链表等):
{ list1[0]+merge(list1[1:],list2) list1[0]<list2[0]
| list2[0]+merge(list1,list2[1:]) otherwise
也就是说,两个链表头部较小的一个与剩下元素的 merge 操作结果合并。
直接将以上递归过程建模,首先考虑边界情况。 特殊的,如果 l1 或者 l2 一开始就是 null ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个的头元素更小,然后递归地决定下一个添加到结果里的值。如果两个链表都是空的,那么过程终止,所以递归过程最终一定会终止。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
4、编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。注意:分割后保证原来的数据顺序不变。
(1)有两个新链表(都需要尾插);
(2)可能有一个链表不存在;
(3)保证最终链表的最后一个结点的next是null。
1)理想情况下,把大链表接到小链表后边。
2)如果没有小链表,直接返回大链表(大链表可能为空)。保证,返回链表的最后一个结点.next == null。
尾插 分情况讨论:
(1)如果当前链表为空,要插入的结点就是链表的第一个结点
(2)如果链表不为空,
1. 先找到当前的最后一个结点
2. 让当前的最后一个结点的 next = 要插入的结点
3. 如果每次的最后一个结点都是我们插入的,可以记录上次插入的最后一个结点。
4. 不要忘记更新最后一个结点
x = 5
public ListNode partition(ListNode pHead, int x) {
ListNode less = null;
ListNode lessLast = null;
ListNode great = null;
ListNode greatLast = null;
ListNode cur = pHead;
while (cur != null) {
if (cur.val < x) {
if (less == null) {
less = cur;
} else {
lessLast.next = cur;
}
lessLast = cur;
} else {
if (great == null) {
great = cur;
} else {
greatLast.next = cur;
}
greatLast = cur;
}
cur = cur.next;
}
if (less == null) {
return great;
} else {
lessLast.next = great;
if (greatLast != null) {
greatLast.next = null;
}
return less;
}
}
5、给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
1)先求出链表的长度len,然后向后移动(len / 2)次即可。
private int getLength(ListNode head) {
int len = 0;
for (ListNode cur = head; cur != null; cur = cur.next) {
len++;
}
return len;
}
public ListNode middleNode(ListNode head) {
int len = getLength(head);
int midLen = len / 2;
ListNode node = head;
for (int i = 0; i < midLen; i++) {
node = node.next;
}
return node;
}
2)定义两个结点:一个结点为fast,每次循环移动两次;另一个为slow结点,每次循环移动一次。
规则:如果链表不为空,则先移动一次fast结点,如果此时fast结点不为空,则再分别移动一次slow结点和fast结点,如果此时fast结点仍不为空,继续循环。只要循环过程中fast结点为空,则跳出循环,返回slow结点即为所求中间结点。
public ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null) {
fast = fast.next;
if (fast == null) {
break;
}
slow = slow.next;
fast = fast.next;
}
return slow;
}
6、输入一个链表,输出该链表中倒数第k个结点。
1)先求出链表的长度len,然后重新遍历向后移动(len - k)次即可。
public ListNode FindKthToTail(ListNode head,int k) {
int len = 0;
for (ListNode c = head; c != null; c = c.next) {
len++;
}
if (len < k) {
return null;
}
int steps = len - k;
ListNode r = head;
for (int i = 0; i < steps; i++) {
r = r.next;
}
return r;
}
2)前后引用遍历。
定义两个结点:一个结点为front结点,先移动k次停止;另一个为back结点,在front结点结束k次移动后开始和front结点同时移动,直到front结点为空结束循环,返回back结点即为所求结点。
public ListNode FindKthToTail(ListNode head,int k) {
ListNode front = head;
ListNode back = head;
for (int i = 0; i < k; i++) {
if (front == null) {
return null;
}
front = front.next;
}
while (front != null) {
back = back.next;
front = front.next;
}
return back;
}
7、链表的回文结构。
先找到链表的中间结点,将后半部分的结点逆置,再与前半部分的结点进行比较。如果有结点不相等则跳出循环,返回false;如果前半部分或者后半部分任一为空时仍没有结点不相等,则链表为回文结构,返回true。
public ListNode getMid(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null) {
fast = fast.next;
if (fast == null) {
break;
}
fast = fast.next;
slow = slow.next;
}
return slow;
}
public ListNode reverse(ListNode head) {
ListNode result = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = result;
result = cur;
cur = next;
}
return result;
}
public boolean chkPalindrome(ListNode A) {
ListNode mid = getMid(A);
ListNode h2 = reverse(mid);
ListNode n1 = A;
ListNode n2 = h2;
while (n1 != null && n2 != null) {
if (n1.val != n2.val) {
return false;
}
n1 = n1.next;
n2 = n2.next;
}
return true;
}
8、在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
定义三个结点:一个结点prev为空;一个结点p1 = head;另一个结点p2 = head.next。
如果p1 != p2,则三个结点同时向后移动。
否则,p1不动,p2走到第一个不相等的结点处,prev.next = p2,p1 = p2,p2 = p2.next。
直到p2为kong。
public ListNode deleteDuplication(ListNode pHead)
{
if (pHead == null) {
return null;
}
ListNode prev = null;
ListNode p1 = pHead;
ListNode p2 = pHead.next;
while (p2 != null) {
if (p1.val != p2.val) {
prev = p1;
p1 = p2;
p2 = p2.next;
} else {
while (p2 != null && p2.val == p1.val) {
p2 = p2.next;
}
if (prev == null) {
pHead = p2;
} else {
prev.next = p2;
}
p1 = p2;
if (p2 != null) {
p2 = p2.next;
}
}
}
return pHead;
}
9、编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表,在结点c1开始相交。
首先排除两个单链表相交不可能出现的情况——形如“×”的交叉。因为单链表的next结点只能指向一个结点,两个链表一旦相交之后,一定重合(如上图所示)。
1)首先计算两个链表的长度,并求得长度差diff。
2)先让较长的链表向后走diff步;
3)然后两个链表同时向后走,如果有两个结点相等的情况,则第一个相遇即为两链表相交。
private int getLength(ListNode head) {
int len = 0;
for(ListNode c = head; c != null; c = c.next) {
len++;
}
return len;
}
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = getLength(headA);
int lenB = getLength(headB);
int diff = lenA - lenB;
ListNode longer = headA;
ListNode shorter = headB;
if(lenA < lenB) {
longer = headB;
shorter = headA;
diff = lenB - lenA;
}
for(int i = 0; i < diff; i++) {
longer = longer.next;
}
while(longer != shorter) {
longer = longer.next;
shorter = shorter.next;
}
return longer;
}
10、给定一个链表,判断链表中是否有环。
快慢指针:快的一次2步,慢的一次1步。如果有环,两者一定相遇;如果没有环,则不会相遇。(快的不能一次3步或者n(n>=3)步,有可能会一直错过)
如果相遇,则链表带环;如果快的遇到null,则不带环,返回null。
public boolean hasCycle(ListNode head) {
//求相遇
// 如果快的遇到null,表示没有环,直接返回null
ListNode fast = head;
ListNode slow = head;
do {
if(fast == null) {
return false;
}
fast = fast.next;
if(fast == null) {
return false;
}
fast = fast.next;
slow = slow.next;
}while(fast != slow);
return true;
}
11、 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL。
方法1:把带环问题转为相交问题。将相遇点的next设为null,将带环链表变成两个不带环的链表,求两个链表的相交点。
方法2:一个引用从起点出发,一个引用从相遇点出发,都直走一步,一定会在环的入口点相遇。
证明:
1.慢引用走的距离 L + C (L表示从起点到入环的第一个入口点的距离,C表示慢引用在环内走过的距离)
2.快引用走的距离 L + C + (n + 1) * R (n >= 0,表示快引用在环内走的圈数;R表示快引用在环内走过的距离)
或 2 * (L + C) = 2 * L + 2 * C
2 * L + 2 * C = L + C + n * R + R - C ==> L = n * R + (R - C)
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
// fast遇到null,表示不带环,返回null
// fast = slow,表示遇到相遇点了
do {
if(fast == null) {
return null;
}
fast = fast.next;
if(fast == null) {
return null;
}
fast = fast.next;
slow = slow.next;
}while(fast != slow);
// 求相遇
// 如果快的遇到null,表示没有环,直接返回null
// 相遇点出发 + 起点出发,最终相遇
ListNode p = head;
ListNode q = slow;
while(p != q) {
p = p.next;
q = q.next;
}
return p;
}
扩展:
“相交 + 带环问题”:求两个链表是否带环或相交。
此问题可以分为六种情况:1)两者均不带环也不相交,2)两者不带环但相交,3)有一条链表带环但不相交,4)两者均带环但不相交,5)两者相交与环外,6)两者相交与环内。
思路分析:
1. 如果两个链表都没有带环,区分是否相交的问题<情况1)和2)>。
2. 一个带环,一个不带环<情况3)>。
3. 两个都带环:求环的入口点
如果是一个交点<情况5)>。
否则,如果两个环的长度不相等<情况4)>。
相等:一个从环入口点A出发,走环的长度,如果能遇到入环点B则为<情况6)>,否则为<情况4)>。
12、给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝(深拷贝)。
首先分析:复制普通链表(浅拷贝)。
public Node copy(Node head) {
Node result = null;
Node resultLast = null;
for(Node cur = head; cur != null; cur = cur.next) {
Node node = new Node();
node.val = cur. val;
if(result == null) {
result = cur;
} else {
resultLast.next = cur;
}
resultLast = cur;
}
return result;
}
本题的结点Node类的定义如下:
class Node {
public int val;
public Node next;
public Node random; // 没有作用
public Node() {}
public Node(int _val,Node _next,Node _random) {
val = _val;
next = _next;
random = _random;
}
}
给定如下图所示的链表:
分三步走:
(1)把原链表变成当前形式,即变为“老 — 新 — 老 — 新 — …”的形式。用p1遍历原链表的每个结点,创建新结点p2,把p2插入到p1的后边。
(2)处理random指向。使p1 = head,p2 = p1.next。
只要p1.random不为空,让p2.random = p1.random.next。
直至p1为空结束循环。
(3)完成复制。创建一个新链表将,新结点一一完成拷贝。
class Solution {
public Node copyRandomList(Node head) {
if(head == null) {
return null;
}
// 1)
Node p1 = head;
while(p1 != null) {
Node p2 = new Node();
p2.val = p1.val;
p2.random = null;
p2.next = p1.next;
p1.next = p2;
p1 = p2.next;
}
// 2)
p1 = head;
while(p1 != null) {
Node p2 = p1.next;
if(p1.random != null) {
p2.random = p1.random.next;
}
p1 = p2.next;
}
// 3)
p1 = head;
Node newHead = head.next;
while(p1 != null) {
Node p2 = p1.next;
p1.next = p2.next;
if(p2.next != null) {
p2.next = p2.next.next;
}
p1 = p1.next;
}
return newHead;
}
}
扩展:
深拷贝 & 浅拷贝:
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化。
深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变。