链表突击:在链表题中,最经典的莫过于反转列表,我们强烈建议您先从这题下手,温故而知新。做完本章节后,相信您对链表的使用能力一定会大大提升!
(*)问题十九:反转链表
问题描述:
反转一个单链表。示例:输入: 1->2->3->4->5->NULL;输出: 5->4->3->2->1->NULL。
解题思路:
三指针法进行迭代。(1)使用pre、head、next指针保存当前指针的上一个指针、当前指针、当前指针的下一个指针。(2)next = head.next;暂存下一个节点;(3)head.next = pre;断链并进行反转;(4)将pre指针和nextr指针往后移动一格。准备继续下一次指针的反转。与剑指Offer的面试题24:反转链表一模一样,但自己还是没有记住答案,第一次解答时至少自己还想出来一个利用堆栈的后进先出原理进行解决,唉..我的提交执行用时已经战胜 90 % 的 java 提交记录,
代码示例:
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next ==null) return head;
ListNode pre = null;
ListNode next = null;
while(head!=null){
next = head.next;//暂存下一个节点
head.next = pre;//断链并进行反转
pre = head;//将两个指针都往后移动一格
head = next;
}
return pre;
}
}
问题二十:两数相加
问题描述:
给出两个非空的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储一位数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。您可以假设除了数字 0 之外,这两个数都不会以 0 开头。示例:输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)输出:7 -> 0 -> 8原因:342 + 465 = 807
解题思路一:
逐位累加、进位,更新每个指针域的val。(1)计算链表的第一个数字的和,注意暂存累加和、进位与首指针。(2)若两个链表都不为null,则循环处理,head.next = new ListNode(add_num);;head = head.next;更新head。(3)考虑两个链表有一个为null的情况,进行处理。(4)考虑最后进位不为零的情况。我的提交执行用时已经战胜 83.96 % 的 java 提交记录。
代码示例一:
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head,tmp;
//计算链表第一个数字
int carry = 0;
int add_num = (l1.val + l2.val + carry)%10;
carry = (l1.val+ l2.val +carry)/10;
head = new ListNode(add_num);
tmp = head;
l1 = l1.next;
l2 = l2.next;
//计算所有数字
while(l1!=null && l2!=null){
add_num = (l1.val + l2.val + carry)%10;
carry = (l1.val+ l2.val +carry)/10;
head.next = new ListNode(add_num);
head = head.next;
l1 = l1.next;
l2 = l2.next;
}
while(l1!=null){
add_num = (l1.val + carry)%10;
carry = (l1.val + carry)/10;
head.next = new ListNode(add_num);
head = head.next;
l1 = l1.next;
}
while(l2!=null){
add_num = (l2.val + carry)%10;
carry = (l2.val + carry)/10;
head.next = new ListNode(add_num);
head = head.next;
l2 = l2.next;
}
if (carry != 0)
head.next = new ListNode(carry);
return tmp;
}
}
解题思路二:
思路大致相同,但代码无疑比我的凝练太多了,主要凝练在:(1)while循环条件使用较好;尽可能减少代码的冗余量;while(l1 != null || l2 != null || carry != 0)(2)没有重复的累加过程,基本只加一次;(3)使用head.next来保存计算后的链表。我的提交执行用时已经战胜 93.66 % 的 java 提交记录。
代码示例二:
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int carry = 0;
ListNode p, head = new ListNode(0);
p = head;
while(l1 != null || l2 != null || carry != 0) {
if (l1 != null) {
carry += l1.val;
l1 = l1.next;
}
if (l2 != null) {
carry += l2.val;
l2 = l2.next;
}
p.next = new ListNode(carry%10);
carry /= 10;
p = p.next;
}
return head.next;
}
}
问题二十一:合并两个有序链表
问题描述:
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。示例:输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4
解题思路一:
新建链表,比较两个有序链表进行赋值。(1)新建链表;(2)两个有序链表都不为空,进行比较,将较小的放到新链表的next域中;(3)如果一个走完另一个没有走完,直接将没走完的放在合并后链表的后面。能解决问题的方法就是好方法,不要想太秀的方法。我的提交执行用时已经战胜 96.47 % 的 java 提交记录。
代码示例一:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode tmp = new ListNode(-1);
ListNode p = tmp;
while(l1!=null && l2!=null){
if(l1.val < l2.val){
tmp.next = new ListNode(l1.val);
l1 = l1.next;
tmp = tmp.next;
}else{
tmp.next = new ListNode(l2.val);
l2 = l2.next;
tmp = tmp.next;
}
}
if(l1!=null) tmp.next = l1;
if(l2!=null) tmp.next = l2;
return p.next;
}
}
(*)解题思路二:
采用递归方法合并。这或许是考察的重点,在一个链表上进行操作,采用递归的方法进行赋值。我的提交执行用时已经战胜 96.47 % 的 java 提交记录。
代码示例二:
class Solution {
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;
}
}
}
(*)问题二十二:合并K个排序链表
问题描述:
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。输入:[ 1->4->5, 1->3->4,2->6];输出: 1->1->2->3->4->4->5->6。
解题思路一:
添加进Arraylist,转换为一维数组并快速排序,生成新链表,完全利用排序的特性。(1)遍历每一个链表,将其添加进刚建立的Arraylist数组中;(2)将Arraylist数组转为一维数组,进行Arrays.sort快速排序;(3)新建链表,将排序后的数组加入。我的提交执行用时已经战胜 70.04 % 的 java 提交记录。
代码示例一:
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ArrayList<Integer> arrays = new ArrayList<>();
for(int i = 0;i<lists.length;i++){
while(lists[i]!=null){
arrays.add(lists[i].val);
lists[i] = lists[i].next;
}
}
int [] list = new int [arrays.size()];
for(int i = 0;i<arrays.size();i++){
list[i] = arrays.get(i);
}
Arrays.sort(list);
ListNode temp = new ListNode(-1);
ListNode result = temp;
for(int i = 0;i<arrays.size();i++){
temp.next = new ListNode(list[i]);
temp = temp.next;
}
return result.next;
}
}
解题思路二:
循环递归,两两递归合并。譬如lists的长度为4,(1)先两两合并,0和3合并,1和2合并。生成新的ListNode数组,新数组长度为2,参考上一道题;(2)再对新数组循环递归合并,0和1合并。生成新的合并后的ListNode。我的提交执行用时已经战胜 95.36 % 的 java 提交记录。
代码示例二:
class Solution {
public ListNode mergeKLists(ListNode[] lists){
int len = lists.length;
if(len==0) return null;
if(len==1) return lists[0];
ListNode[] res = new ListNode[(len+1)/2];
int i = 0,j = len-1;
while (i<j){
res[i] = mergeTwoLists(lists[i],lists[j]);
i++;
j--;
}
if(i==j){
res[i] = lists[i];
}
return mergeKLists(res);
}
private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null) return list2;
if (list2 ==null) return list1;
if(list1.val<=list2.val){
list1.next = mergeTwoLists(list1.next,list2);
return list1;
}else{
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}
}
}
(*)问题二十三:旋转链表
问题描述:
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。示例 1: 输入: 1->2->3->4->5->NULL, k = 2 输出: 4->5->1->2->3->NULL。解释: 向右旋转 1 步: 5->1->2->3->4->NULL;向右旋转 2 步: 4->5->1->2->3->NULL。示例 2: 输入: 0->1->2->NULL, k = 4 输出: 2->0->1->NULL。
解题思路一:
构键循环链表,计算位置关系(总长度-向右移动位置/总长度)。(1)遍历一遍链表,链表首部和尾部元素,构成循环链表;(2)循环链表断为单链表,计算循环链表断链的位置和应该移动的位置。需要移动的位置长度为count-k%count;保留该位置的下一个元素,在该移动位置处断链(主要是找到向右移动 k 个位置和循环链表移动位置之间的关系)。我的提交执行用时已经战胜 98.30 % 的 java 提交记录。
代码示例一:
class Solution {
public ListNode rotateRight(ListNode head, int k) {
ListNode tmp = head;
if(tmp == null) return null;
if(tmp.next == null ) return tmp;
if(k==0) return tmp;
int count = 1;
while(tmp.next!=null){
tmp = tmp.next;
count++;
}
tmp.next = head;
tmp = tmp.next;
int i = 1;
while(i<count-k%count){
tmp = tmp.next;
i++;
}
ListNode result = tmp.next;
tmp.next = null;
return result;
}
}
解题思路二:
思路相同,更为凝练。
代码示例二:
public static ListNode rotateRight(ListNode head, int k) {
ListNode res;
ListNode p = head;
int num = 0;
if (head == null) return null;
//将单链表变为循环链表
for (; p.next != null; p = p.next, num++) ;
num++;
p.next = head;
if (k > num) {
k = k % num;
}
num -= k;
p = head;
for (; num > 0; num--)
p = p.next;
res = p;
//将循环链表改为单链表
ListNode q = head;
for (; q.next != res; q = q.next) ;
q.next = null;
return res;
}
(*)问题二十四:环形链表
问题描述:
给定一个链表,判断链表中是否有环。为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。示例 1:输入:head = [3,2,0,-4], pos = 1输出:true解释:链表中有一个环,其尾部连接到第二个节点。
解题思路一:
双指针(快慢指针)技术,不应该不会啊。分为快指针、慢指针。快指针fast一次走两格,慢指针slow一次走一格。若两针相等,则相遇,说明是有环,否则无环。因为fast指针每次前进两个,一定比slow指针先到达环的入口处。而当slow指针进入环的时候,fast指针已经经过了两个节点,如下图所示。这个时候,我们将这个过程想象成400m跑步的追及问题。如果存在环的话,因为fast指针速度更快,一定会追上slow指针。记住最后一个指针为null,null的后面为空指针异常。击败93%。参考:
代码示例一:
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast)
return true;
}
return false;
}
}
解题思路二:
HashSet大法好。遍历一遍,每次访问一个节点首先都判断Hashset中是否存在同一个节点,如果存在则链表存在环,否则持续加入到Hashset中。当指针遍历到NULL的时候仍然没有找到环。
代码示例二:
public boolean hasCycle(ListNode head) {
Set<ListNode> set = new HashSet<ListNode>();
while(head != null){
if(set.contains(head)){
return true;
}
set.add(head);
head = head.next;
}
return false;
}
问题二十五:环形链表 II
问题描述:
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。说明:不允许修改给定的链表。 输入:head = [3,2,0,-4], pos = 1;输出:tail connects to node index 1;解释:链表中有一个环,其尾部连接到第二个节点。
解题思路一:
快慢指针技术进阶版,发现一旦快指针与慢指针相遇时,从第一个指针出发,慢指针与第一指针每次走一格,当两者相等时,就会达到入口点。我的提交执行用时已经战胜 99.39 % 的 java 提交记录。
如下图所示,X,Y,Z分别为链表起始位置,环开始位置和两指针相遇位置,则根据快指针速度为慢指针速度的两倍,可以得出:
2*(a + b) = a + b + n * (b + c);即 a=(n - 1) * b + n * c = (n - 1)(b + c) +c;
注意到b+c恰好为环的长度,故可以推出,如将此时两指针分别放在起始位置和相遇位置,并以相同速度前进,当一个指针走完距离a时,另一个指针恰好走出 绕环n-1圈加上c的距离。故两指针会在环开始位置相遇。参考:
代码示例一:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
ListNode firstpos = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
while(firstpos!=slow){
firstpos = firstpos.next;
slow = slow.next;
}
return slow;
}
}
return null;
}
}
解题思路二:
HashSet大法好。同上面那个题一模一样。
代码示例二:
同上一题的代码示例二。
(*)问题二十六:相交链表
编写一个程序,找到两个单链表相交的起始节点。如下面的两个链表:
解题思路一:
哈希大法好。可惜时间复杂度和空间复杂度大了一些。先将headA存储至哈希表中,再使用contains进行判断。我的提交执行用时已经战胜 12.64 % 的 java 提交记录。
代码示例一:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
HashSet<ListNode> hashset = new HashSet<>();
if(headA == null || headB == null) return null;
while(headA!=null){
hashset.add(headA);
headA = headA.next;
}
while(headB!=null){
if(hashset.contains(headB)){
return headB;
}
headB = headB.next;
}
return null;
}
}
解题思路二:
一二三一起走。思路可以说比代码什么的更重要。为什么自己看不到规律?假设一个链表长X,另一个链表长y(x>=y),第一个链表先走x-y步,然后再一起走....。我的提交执行用时已经战胜 96.32 % 的 java 提交记录。参考:
代码示例二:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode poiointer_A = headA,pointer_B = headB;
int countA = 0,countB = 0,sub = 0;
if(headA == null || headB == null) return null;
while(poiointer_A!=null){
poiointer_A = poiointer_A.next;
countA++;
}
while (pointer_B!=null){
pointer_B = pointer_B.next;
countB++;
}
sub = Math.abs(countA-countB);
while (sub!=0){
if(countA>countB){
headA = headA.next;
}else{
headB = headB.next;
}
sub-=1;
}
while (headA!=null && headB!=null){
if(headA== headB){
return headA;
}else{
headA = headA.next;
headB = headB.next;
}
}
return null;
}
}
(*)问题二十七:删除链表中的节点
问题描述:
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。输入: head = [4,5,1,9], node = 5;输出: [4,1,9];解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
解题思路:
节点赋值。将待删除节点的值赋值为下一个节点的值,然后将待删除节点的引用指向待删除节点的下下个节点。还以为题出错了,想着为什么不给链表的头结点。唉..心累。
代码示例:
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
}