本文是Leetcode刷题计划第一周的总结。另外,还包括了一些课堂上学习到的算法的练习,下面将围绕每一道题进行分析。
InPlace Merge
第一道题是归并排序。归并排序基本思路是使用分治法进行求解。这里的关键操作在于Merge两个有序的数组。通常的做法是,付出O(n)空间复杂度的代价,新建一个数组,然后使用两个指针分别遍历原数组的左右两边,依次选择较小的值放到新数组,最后复制新数组的值到原数组。(也可以一开始复制原数组左右两边到两个小数组,然后遍历依次放到原数组中)
这里面我们付出了O(n)的空间复杂度和O(n)的时间复杂度的代价。能否使得空间复杂度降低为O(1)?
内存反转
这里使用的是循环移位的策略,也称作内存反转。如下所示:
Q:给定序列,a1,a2,…,am,b1,b2,…,bn,把它变成b1,b2,…,bn,a1,a2,…,am?
A:先对a1,a2,…,am进行反转;再对b1,b2,…,bn进行反转,最后再对整体进行反转,就可以得到b1,b2,…,bn,a1,a2,…,am。代码如下:
1 | void exchange(vector<int> &A, int s, int m, int e){ |
原地归并
假设数据如下图所示:
开始时i,j分别指向这个数组的两个有序子序列的第一个值,然后指针向后移动,直到找到比20大的值,即移动到30,此时我们知道i指针之前的值一定是两个子序列最小的块。
接着,先用一个临时指针记录j的位置。然后把第二个序列的指针j向后移动,直到找到比30大的值,即移动到55,即如下图所示:
这样,我们把[i,index)和[index,j)的内存块进行交换,再移动i指针,移动步长step=j-index,两个子数组的分界点mid也要相应的向后移动step=j-index。如下图:
这样可以看出i之前的都已经排好序,而以i开始的子序列和以j开始的子序列又是开始的问题模型,同样的操作进行下去最终排序完成。
1 | void inplaceMerge(vector<int> &A, int start, int mid, int end){ |
Two Sum (#1)
问题:给定一个无序数组,在O(n)时间内寻找两个数的和为target,返回这两个数的下标。题目已经保证了结果是唯一的。
分析:如果是O(n^2)复杂度,那么只要两个循环遍历就能找到。另外,O(n)时间复杂度意味着不能排序,返回下标也意味着最好不要打乱原数组的顺序。我们联想到哈希表获取元素的时间复杂度为O(1),如果将数据处理成target-某个数=另一个数,那么使用哈希表查找就能查找到另一个数(key:原始数据,value:下标)。但是由于题目没有保证元素都不相同,因此不能先循环,处理每个数存入哈希表(相同的数会覆盖)。这里使用的是边遍历边存储到哈希表,如果在哈希表中找到另一个数,则直接返回结果,否则将该数存入哈希表中。代码如下:
1 | class Solution { |
变型:如果加上一个条件原数组是有序的,那么可以使用另外一种方法找到所有和为target的数据对。
分析:可以使用两个指针i,j,分别指向有序数组的低端和高端,如果两个指针所在位置的数据之和为target,则加入结果集。如果和小于target,说明i指向的数太小了,必须稍微大一点才可以;或者说j指向的数不够大,任凭j怎么往左移都没用,因为j指向的数最大。因此向右移动i指针。如果和大于target,说明j指向的太大了,因此往左移动j指针。
为了去掉重复的结果,需要在和等于target的条件里,移动两个指针,直到遇到不一样的数。
1 | public static List<int[]> twoSum(Integer[] nums, int target) { |
Three Sum (#15)
问题:给定一个数组,在O(n^2)复杂度内找出任何3个和为0的数,返回数据对即可。
分析:该题可以进行排序求解,返回数据而不是下标。可以利用Two Sum中变型策略。首先固定一个数,然后使用Two Sum求解。即,使用for循环遍历数组,固定当前数,然后找到所有和当前数搭配的和0的其余两个数,即twoTarget = -nums[i]。具体代码如下:
1 | public static List<int[]> threeSum(Integer[] nums) { |
注意上述去重操作。
Swap Elements By Xor
可以使用异或操作交换数据。原理是使用异或的性质:
- 任意两个相同的数异或都为0
- 任意数(1或者0)和0异或都为本身。1 xor 0 = 1, 0 xor 0 = 1;因此式子中有0的地方可以直接去掉0。
- 异或满足结合律和交换律。
1
2
3
4
5
6void swap(int &x, int &y){
if(x == y) return;
x ^= y; // x = x ^ y
y ^= x; // y = x ^ y ^ y = x ^ (y ^ y) = x ^ (0) = x
x ^= y; // x = x ^ y ^ x = y
}
运行原理如上述代码注释部分。
这里有一个陷进,如果两个相同的数传入,实际上交换完没变。但是因为相同的数异或为0,使得x和y都等于0了。因此最好要一开始检查一下是否相同。
Number of 1 Bits(#191)
统计位当中1的个数。正常想法就是看看最后1位是不是1,是就累加。然后不断往右移位,直到数变成0为止。如下:
1 | int hammingWeight(uint32_t n){ |
另一种方法,利用n&=(n−1),该式子会将n最右边的1变成0。如果本身为0,与之后肯定为0不变;如果最右边是1,减一后为0,再&之后就变成0了。
1 | int hammingWeight(uint32_t n) { |
Count Bits(#338)
问题:统计range(n)范围内所有数当中位为1的个数。要求时间复杂度为O(n)。
分析:正常会对每个数求1的个数,复杂度为O(n * sizeof(integer))。这里由于要求出前面数的1的个数,因此可以利用前面的数,动态规划思想。将大的数化成前面小的数。一种方法是,单独抽出最后一位,则原始的数可以往右移1位变成前面的数。
1 | vector<int> countBits(int num) { |
另一种,化大数为前面的数,是利用前面提到的将某个数位当中最右边1化为0.则:
1 | result[i] = result[i & (i-1)] + 1; |
Reverse Bits (#190)
问题:反转数的二进制位。
分析:可以新建一个数m=0,m先左移一位腾出一个位置,然后从原始数的最后一位开始,不断用m求或运算。直到原始数位全部遍历完毕。复杂度为O(sizeof(int)).
1 | uint32_t reverseBits(uint32_t n) { |
另外,还有个O(log sizeof(int))复杂度的算法如下:
1 | uint32_t reverseBits(uint32_t n) { |
Reverse Integer(#7)
问题:反转整数
分析:新建一个数result初始化为0,每次取出最后一位,使用result10+最后一位。即可得到逆序。例如:123: ( (0\10+3)* 10)+2)*10 + 1 = 321. 注意溢出的处理,策略是:将求得的结果tmp=result*10+最后一位,根据:(tmp-最后一位)/10,逆序求出result,看看跟原来的result是否相等,不相等说明溢出了。
1 | int reverse(int x) { |
Judge Route Circle(# 657)
问题:机器人上下左右移动,给出一个移动序列,判断是否回到出发点。
答:两个数分别统计上下和左右情况,上下抵消,左右抵消。最后都为0则回到原点。
1 | bool judgeCircle(string moves) { |
Merge Two Sorted Array(#88)
问题:给定两个有序的数组nums1,nums2,要求将nums2 Merge到nums1,使得nums1保持有序。只能利用nums1空间,题目已经保证nums1能保证容纳的下nums2的元素。
分析:常规想法是从第一个数开始merge到nums1中,这样就需要移数操作。如下:
1 | void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) { |
上述复杂度为O(n^2)。另一种想法是从尾部开始merge。两个指针分别指向两个数组的最后一个元素,将二者中大的数挪到nums1最后一个位置p=m+n-1,一直到p=0或者nums2已经全部放到nums1中了,则停止循环。
1 | void merge(vector<int>& A, int m, vector<int>& B, int n) { |
还可以利用上面提到到的Inplace Merge方法,将nums2的元素直接全部放到nums1尾部,然后调用InplaceMerge方法。
Merge Two Sorted List(#21)
问:将两个有序链表merge到一起。
答:一种思路是将list2 Merge到list1,不断重新构造list1即可。如下:
1 | ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { |
还有种方法是,新建一条新的链表,这个空间复杂度实际上是O(1),除了浪费一个头节点以外,其余的节点都是list1和list2上已经存在的节点。
1 | ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { |
Merge Two Binary Trees(#617)
问题:将两个二叉树Merge起来,如果位置相同则数据相加,否则添加该节点到新树上。如下图:
分析:显然这题需要使用递归操作。我们目标是将Tree2 Merge到Tree1上。首先,任意一棵树为NULL,则返回令一颗树即可,都不为空,则val相加,然后递归调用merge两棵树的左子树,merge两棵树的右子树。merge左子树时,如果Tree1的左子树为空,则需要将Tree2的左子树接到Tree1的左子树上。merge右子树时,如果Tree1的右子树为空,则需要将Tree2的右子树接到Tree1的右子树上。
1 | struct TreeNode { |
Hamming Distance(#461)
问题:求两个序列的汉明码距离。
分析:汉明码距离就是二进制相应位置上不同数的总数。比如 0001和0100,第2位和第4位不同,则距离为2。求解很简单,两个数求异或。然后求异或结果的1的个数即可。
1 | /* 先异或操作,然后统计1的个数。*/ |