142. 环形链表 II

问题

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。

进阶:
你是否可以不用额外空间解决此题?

解法

我们在环形链表的问题中知道,可以使用快慢指针来判断一个链表是不是有环。但是该题目是需要返回链表的入环节点,仅使用快慢指针只是判断出了是否有环,所以还需要进一步的逻辑才能获取到入环节点。
假设链表有环,设入环点的为\(A\),从\(head\)到\(A\)的距离为\(a\),设快慢指针相交点为\(B\),从\(A\)到\(B\)的距离为\(b\),此时可得,慢指针走过的距离为\(a+b\),同时,我们知道快指针走过的距离是慢指针的一倍,即\(2*(a+b)\),此时可得如下结论,

  • 慢指针再走\(a+b\)得距离就回到\(B\)点
  • 慢指针再走\(a\)的距离就到了\(A\)点
    第一点对我们解本题没有什么用,看第二点,我们在一开始定义的\(a\)就是从\(head\)到入环点\(A\)的距离,那么我们可以将快指针重置到\(head\)处,然后快慢指针同时前进,当快慢指针相等时,就是入环点,返回即可。

代码

java实现

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) {
            return null;
        }
        //使用快慢指针的方式进行
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                //此时说明有环,需要处理
                fast = head;
                while(true) {
                    if (slow == fast) {
                        return slow;
                    }
                    slow = slow.next;
                    fast = fast.next;
                }
            }
        }
        //无环状态
        return null;
    }
}
2019/01/16 21:59 下午 posted in  leetcode

86. 分隔链表

问题

给定一个链表和一个特定值 \(x\),对链表进行分隔,使得所有小于 \(x \)的节点都在大于或等于\(x\)的节点之前。

你应当保留两个分区中每个节点的初始相对位置。

示例:

输入: \(head = 1->4->3->2->5->2, x = 3\)
输出: \(1->2->2->4->3->5\)

解法

新建两个\(dummyHead\),其中一个保存所有小于\(x\)的节点,另一个保存所有大于等于\(x\)的节点。最后拼接两个链表就行了。

代码

java实现

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode partition(ListNode head, int x) {
        ListNode dummy1 = new ListNode(1);
        ListNode dummy2 = new ListNode(1);
        ListNode node1 = dummy1;
        ListNode node2 = dummy2;
        while(head != null) {
            if (head.val < x) {
                node1.next = head;
                head = head.next;
                node1 = node1.next;
                node1.next = null;
            } else {
                node2.next = head;
                head = head.next;
                node2 = node2.next;
                node2.next = null;
            }
        }
        node1.next = dummy2.next;
        return dummy1.next;
    }
}

整体也没什么难度。需要注意的就是在

2019/01/16 21:21 下午 posted in  leetcode

88. 合并两个有序数组

问题

给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。

说明:

  • 初始化 nums1 和 nums2 的元素数量分别为 m 和 n。
  • 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例:

输入:
\(nums1 = [1,2,3,0,0,0], m = 3\)
\(nums2 = [2,5,6], n = 3\)

输出: \([1,2,2,3,5,6]\)

解法

如果正序对比,那么会涉及到\(nums1\)数组的元素右移,时间复杂度非常高。直接排除掉这种做法。
考虑倒序对比,会发现这样一种情况,最终合并到\(nums1\)数组中的数据,最大值的位置是在\(m+n-1\)处,而通过观察可以发现,\(m+n-1\)是必然大于等于\(m-1\)的。这样在倒序合并时,并不会覆盖掉\(nums1\)的原始数值的。这里的循环跳出条件是,\(nums1\)已经扫描完了,或者\(nums2\)已经扫描完了。但是需要注意的一点是,跳出后,如果是\(nums2\)扫描完了,那么可以直接返回;如果是\(nums1\)扫描完了,仍然需要把\(nums2\)的数据写回到\(nums1\)中去。

代码

java实现

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i=m+n-1;
        while(m>0 && n>0) {
            if(nums1[m-1] >= nums2[n-1]){
                nums1[i--] = nums1[m-1];
                m--;
            } else {
                nums1[i--] = nums2[n-1];
                n--;
            }
        }
        while(n>0) {
            nums1[i--] = nums2[n-1];
            n--;
        }
    }
}

代码整体没有难度。注意哪个指针往前移就可以了。

2019/01/16 19:42 下午 posted in  leetcode

19. 删除链表的倒数第N个节点

问题

给定一个链表,删除链表的倒数第 \(n\) 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明:

给定的 \(n\) 保证是有效的。

进阶:

你能尝试使用一趟扫描实现吗?

解法

使用双指针来处理。\(fast\)指针先走\(n\)步,然后\(fast\)指针与\(slow\)指针同时推进,当\(fast\)指针走到链表尾时,\(slow\)指针正好走到倒数\(n+1\)的位置。这时候只需要\(slow.next = slow.next.next\)即可,但是如果直接使用\(head\)作为开头有很多的不便。比如\(n\)等于链表长度,此时,要删除的其实为\(head\)节点,但是此时很难去判断究竟是要删除\(slow\)节点,还是\(slow.next\)节点。
为了排除这种情况,我们引入一个\(dummy\)节点,\(dummy.next = head\),在删除时永远只删除\(slow.next\),这样我们无论怎么处理链表,最终链表的\(head\)均为\(dummy.next\)。

代码

java实现

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = dummy;
        ListNode slow = dummy;
        //fast节点先走n步,此处不需要考虑fast.next是否为null,题目保证了n是个有效的数字。
        for(int i = 1; i <= n; i++) {
            fast = fast.next;
        }
        //开始同时推进fast与slow,这里循环跳出条件是fast.next ==null,即fast已经到了end。
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        //此时slow已经到了倒数n+1的位置。但是仍然需要判断slow.next是否为null,防止NPE
        if (slow.next != null) {
            slow.next = slow.next.next;
        }
        //dummy.next为head。返回即可
        return dummy.next;
        
    }
}
2019/01/15 11:25 上午 posted in  leetcode

75. 颜色分类

问题

给定一个包含红色、白色和蓝色,一共 \(n\) 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 \(0、 1 和 2 \)分别表示红色、白色和蓝色。

注意:
不能使用代码库中的排序函数来解决这道题。

示例:

输入: \([2,0,2,1,1,0]\)
输出: \([0,0,1,1,2,2]\)

进阶:

一个直观的解决方案是使用计数排序的两趟扫描算法。

  • 首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
  • 你能想出一个仅使用常数空间的一趟扫描算法吗?

解法1(偷懒的办法)

进阶中给出了一种解决方案。两次循环就可以完成。

代码1

java实现

class Solution {
    public void sortColors(int[] nums) {
        if (nums==null || nums.length<2) {
            return;
        }
        //数组中0的数量
        int i=0;
        //数组中1的数量
        int j=0;
        //数组中2的数量不需要计算,因为当i与j都是0时,剩下的肯定都是2
        //开始第一次循环,获取i与j
        for(int n:nums) {
            if (n==0) {
                i++;
            } else if (n==1) {
                j++;
            }
        }
        //开始第二次循环,将0,1,2重新写入数组
        for(int l = 0;l<nums.length;l++) {
            if (i>0) {
                nums[l]=0; 
            } else if (j>0) {
                nums[l] = 1;
            } else {
                nums[l] = 2;
            }
        }
   }
}

解法2

这其实就是荷兰国旗算法。算法如下:

  • 设置三个指针,\(begin=0,end=nums.length,curr=0\),接下来循环时分为三种情况处理:
    • 当\(nums_{curr} == 0\)时,交换\(begin\)与\(curr\)的值,并且\(begin++;curr++\)
    • 当\(nums_{curr} == 1\)时,不做任何处理,\(curr++\)
    • 当\(nums_{curr} == 2\)时,交换\({end}\)与\(curr\)的值,并且\(end--\)

整体算法比较直观,需要注意的一点是,当前值为\(2\)时,\(curr\)并不往前推进,而是停留在原地。这个的原因在于,如果\(nums_{end}=0\),在本次交换后,\(nums_{curr}=0\),下一轮需要将\(0\)交换给\(begin\)。因此,这种情况下\(curr\)不能往前推进。
还有一个需要注意的地方在于,就是循环的终止条件,如果没有思考清楚,那么很有可能就写成了\(curr<end\),但是这种情况是错的。考虑这样一个输入数组\([2,0,1]\),在第一轮的判断时,是走的如上第三种情况,这样\(end\)在第二轮时就是1了,如果是\(curr<end\)的判断的话,那么这一步就跳出循环了。最终结果就是\([1,0,2]\),第二个元素并没有参与到比较中来。所以循环的规则应该是\(curr<=end\)。

代码2

java实现

class Solution {
    public void sortColors(int[] nums) {
        if (nums == null || nums.length < 2) {
            return;
        }
        //三个指针的初始化,
        int begin = 0, curr = 0, end = nums.length-1;
        //循环的判断条件见解法中的说明
        while (curr <= end){
            //第一种情况
            if (nums[curr] == 0) {
                nums[curr] = nums[begin];
                nums[begin] = 0;
                curr++;
                begin++;
            } else if (nums[curr] == 1) {
                //第二种情况
                curr++;
            } else {
            nums[curr] = nums[end];
            nums[end] = 2;
            end--;
        }
   }
}

解法3

2019/01/12 19:31 下午

61. 旋转链表

问题

给定一个链表,旋转链表,将链表每个节点向右移动 \(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->0->1->NULL\)
向右旋转 2 步: \(1->2->0->NULL\)
向右旋转 3 步: \(0->1->2->NULL\)
向右旋转 4 步: \(2->0->1->NULL\)

解法1

一开始可以发现,有如下三种特殊情况是可以直接不进行任何处理,直接返回head的:

  • \(head\)为\(null\),整个链表都是\(null\)
  • \(head\)的\(next\)为空,代表链表只有一个元素,此时不论右移多少次,都是本身
  • \(k=0\)

考虑\(k\)的取值, 如果\(k\)的值正好是链表长度的整数倍,这种情况右移后还是原链表本身。返回即可。
接下来,如果\(k\)远远超过链表的长度,我们也没必要进行过多的右移,获取\(k\)对链表长度的余数即为右移的次数。
一个难点在于,在单向链表中找到一个元素的父亲节点的时间复杂度很高。此时第一个想法是翻转链表、新链表左移、再次翻转链表。
也写出了第一版代码。

代码1

java实现

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        //三种情况,链表为空、链表只有一个元素、移动位置为0
        if (head == null ||head.next == null || k == 0) {
            return head;
        }
        ListNode tail = head;
        ListNode temp = head;
        //判断长度
        int i = 1;
        while (temp.next != null) {
            temp = temp.next;
            i++;
        }
        
        k = k % i;
        //当旋转整数倍时,没有必要挪动数据,直接返回head
        if (k == 0) {
            return head;
        }
        
        //翻转链表
        head = reverse(head);
        while(k>0) {
            tail.next = head;
            tail = head;
            head = head.next;
            tail.next = null;
            k--;
        }
        head = reverse(head);
        return head;
        
    }
    
    //翻转链表
    ListNode reverse (ListNode head) {
        ListNode prev = head;
        ListNode curr = head.next;
        while (curr!=null) {
            ListNode temp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = temp;
        }
        head.next = null;
        return prev;
    }
}

解法2

解法1的问题在于有两次翻转链表,虽然只是\(O(n)\)的时间复杂度,但是本着能少一步是一步的原则,还是能省则省。
继续观察示例1,发现,右移\(2\)位,实际上是左移\(3\)位,而左移是解法1里就实现了的。这就不需要翻转链表就可以直接移动了。

代码2

java实现

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        //三种情况,链表为空、链表只有一个元素、移动位置为0
        if (head == null ||head.next == null || k == 0) {
            return head;
        }
        ListNode tail = head;
        ListNode temp = head;
        //判断长度
        int i = 1;
        while (tail.next != null) {
            temp = tail;
            tail = tail.next;
            i++;
        }
        
        k = k % i;
        //当旋转整数倍时,没有必要挪动数据,直接返回head
        if (k == 0) {
            return head;
        }
        //增加了一步k的计算
        k = i-k;
        
        //翻转链表
        //注释掉解法1的翻转链表
        //head = reverse(head);
        while(k>0) {
            tail.next = head;
            tail = head;
            head = head.next;
            tail.next = null;
            k--;
        }
        //注释掉解法1的翻转链表
        //head = reverse(head);
        return head;
        
    }
    
    //翻转链表
    ListNode reverse (ListNode head) {
        ListNode prev = head;
        ListNode curr = head.next;
        while (curr!=null) {
            ListNode temp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = temp;
        }
        head.next = null;
        return prev;
    }
}

仔细观察代码,实际上是去掉了翻转链表,对k进行了更进一步的操作而已。相比解法一,代码更加的清晰。

2019/01/12 00:03 上午 posted in  leetcode

27. 移除元素

问题

给定一个数组\( nums\) 和一个值 \(val\),你需要原地移除所有数值等于 \(val\) 的元素,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 \(O(1)\) 额外空间的条件下完成。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1:

给定 \(nums = [3,2,2,3], val = 3\),
函数应该返回新的长度 \(2\), 并且 \(nums\) 中的前两个元素均为 \(2\)。
你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 \(nums = [0,1,2,2,3,0,4,2], val = 2\),
函数应该返回新的长度 \(5\), 并且 \(nums\) 中的前五个元素为 \(0, 1, 3, 0, 4\)。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

解法

使用双指针,从数组的两头分别扫描,左指针停在等于\(val\)的位置,右指针停在不等于\(val\),然后交换两个指针的值,进行下一轮扫描。

代码

java实现

class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            //左指针停在等于val的位置
            while (left < nums.length && nums[left] != val) {left++;}
            //右指针停在不等val的位置
            while (right > 0 && num[right] == val) {right--;}
            //判断两个指针的位置是不是正确,正确的话就交换,否则说明已经超过位置了,直接跳出即可
            if (left<right) {
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
            } else {
                break;
            }
        }
        return left;    
    }
}
2019/01/11 21:43 下午 posted in  leetcode

26. 删除排序数组中的重复项

问题

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 \(O(1)\) 额外空间的条件下完成。

示例 1:

给定数组\( nums = [1,1,2]\),
函数应该返回新的长度 \(2\), 并且原数组 \(nums\) 的前两个元素被修改为 \(1, 2\)。
你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 \(nums = [0,0,1,1,1,2,2,3,3,4]\),
函数应该返回新的长度 \(5\), 并且原数组 \(nums\) 的前五个元素被修改为 \(0, 1, 2, 3, 4\)。
你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝

int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

解法

快慢指针,慢指针只有在快指针的数大于慢指针前一个数时才推进。

代码

java实现

class Solution {
    public int removeDuplicates(int[] nums) {
        int index=0;
        for(int n:nums) {
            if(index<1||n>nums[index-1]) {
                nums[index++]=n;
            }
        }
        return index;
    }
}
2019/01/11 20:38 下午 posted in  leetcode

80. 删除排序数组中的重复项 II

问题

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 \(O(1)\) 额外空间的条件下完成。

示例 1:

给定 \(nums = [1,1,1,2,2,3]\),

函数应返回新长度 \(length = 5\), 并且原数组的前五个元素被修改为 \(1, 1, 2, 2, 3\) 。

你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 \(nums = [0,0,1,1,1,1,2,3,3]\),

函数应返回新长度 \(length = 7\), 并且原数组的前五个元素被修改为 \(0, 0, 1, 1, 2, 3, 3\) 。

你不需要考虑数组中超出新长度后面的元素。
说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}

解法

使用快慢指针,快指针每次循环都往前推进,慢指针只有在指针位置小于2(前面两位必然需要向前推进)或者慢指针前推2位的数小于快指针的数时,才向前推进,并且将快指针的值赋值到慢指针位置。

源码

class Solution {
    public int removeDuplicates(int[] nums) {
        int fast=0;
        int slow=0;
        while(fast<nums.length) {
            //慢指针只有在小于2或者前数2位小于快指针时才往前推进
            if (slow < 2 || nums[fast] > nums[slow-2]) {
                nums[slow] = nums[fast];
                slow++;
            }
            fast++;
        }
        return slow;
    }
}
2019/01/10 23:35 下午 posted in  leetcode

287. 寻找重复数

问题

给定一个包含 \(n + 1\) 个整数的数组 \(nums\),其数字都在 \(1\) 到 \(n\) 之间(包括 \(1\) 和 \(n\)),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

输入: \([1,3,4,2,2]\)
输出: \(2\)

示例 2:

输入: \([3,1,3,4,2]\)
输出: \(3\)

说明:

不能更改原数组(假设数组是只读的)。
只能使用额外的 \(O(1)\) 的空间。
时间复杂度小于 \(O(n^2)\) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。

解法

查看题目,其数字均在\(1\) 到 \(n\) 之间,因此,可以将数字当作数组下标,指向下一个数组元素,那么就可以把数组\(nums\)当成一个链表,题目就成为了将查询链表的环的入口。
链表判断环可以使用快慢指针。

代码

java实现

class Solution {
    public int findDuplicate(int[] nums) {
        int fast = 0;
        int slow = 0;
        while (true) {
            fast = nums[nums[fast]];
            slow = nums[slow];
            if (fast == slow) {
                fast = 0;
                while(fast != slow) {
                    fast = nums[fast];
                    slow = nums[slow];
                }
                return nums[slow];
            }
        }
    }
}
2019/01/10 19:58 下午 posted in  leetcode