题目:
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
分析题目
从一个整数数组中,找到和为特定值的两个数,需要计算每两个数的和,首先可以想到的一种方法是,直接对这个数组 arr 进行从前往后的查找,用两个索引,i , j 分别表示被加数和加数再这个数组中的索引:
- 固定被加数 arr[i],即 i 不变,加数 arr[j] 从被加数所在的索引后面一个元素开始进行遍历,即 j in range(i+1, len(arr)),若在遍历过程中这两个数的和已经等于 target 了,那么直接返回 i 和 j,即是所求
- 当 j 已经遍历到数组的最后一个索引,时,说明被加数 arr[j] 没有与之匹配的加数,变化被加数,将 i 索引向后移动一位,即 i += 1,重复步骤 1,直到找到符合条件的 arr[i] 和 arr[j]
最坏的情况是需要的被加数和加数在数组的末尾,因此程序的时间复杂度是 O(n) = n * log(n)。
根据上面的算法步骤,可以给出如下的代码:
arrLen = len(arr)
for i in range(0, arrLen):
for j in range(i+1, arrLen):
if arr[i] + arr[j] == target:
return [i, j]
return []
这种方法最容易实现,占用的时间和内存如下:
上面的方法看上去并不难,但一开始我并不是这样做的。如果只是想要单纯的做出这个题,那么用上面的方法就已经足够了。
复杂的解法
最初我做这个题的想法是,求给定的数组中的两数之和,如果这个数组已经是个排好序的数组,查找起来应该会快很多,所以可以先将数组 arr 进行排序,排完序后再进行查找。
这个做法的难点是,最后要输出找到的数字的索引,而排序之后原来的数组顺序就可能会被打乱,所以排序的过程中需要添加一个信息用来存储该数据在原来的数组中的索引。
# 准备一个更大的数组,用于保存原索引和数值信息
indices = [0] * len(nums) * 2
for i, n in zip(range(len(nums)), nums):
indices[i*2] = i
indices[i*2+1] = n
现在回想起来,这个做法实在是太愚蠢了,把一个简单的问题想得过于复杂,以至于在 debug 阶段反复出现错误的情况。不过好在这之中我重新捡起了当时在数据结构课程中没有弄懂的快速排序,也算有些收货。
有很多种方法可以实现排序,如冒泡排序,快速排序等等。
冒泡排序
冒泡排序是最先能够想到的,这种排序方法比较低效,但容易实现。排序后的数组随着索引递增,数值越来越大。(需要注意的是,这个扩大后的数组里每两个数据为原数组中的一项)
每进行一次迭代,最大数值的那个数就被排到最后,当排好序的数的个数等于数组长度时,结束循环:
def bubbleSort(indices):
# sort
swap = None
sort_count = 0
l_indices = len(indices)
while sort_count < l_indices:
for j in range(1, l_indices - sort_count, 2):
if indices[j] > indices[j+1]:
swap = indices[j:j+1]
indices[j:j+1] = indices[j-1:j]
indices[j-1:j] = swap
sort_count += 2
冒泡排序是比较低效的,在提交答案时,使用冒泡排序的方法会超时,因此需要换用高效的排序算法。
快速排序
快速排序的根本思想可以归结为二分法,每次迭代时,确定一个用作基准的数,然后用两个“哨兵” i, j分别从数组头部和尾部向相反方向进行移动(遍历),
- 首先移动 j,当 j 所在位置的数比基准数小时(注意不考虑相等情况),固定 j,进行第 2 步,否则继续移动 j,
- 移动 i,当 i 所在位置的数比基准数大时,固定 i,进行第 3 步,否则继续移动 i
- 交换 i j 所在位置的数
- 当 i j 指向数组中的同一个位置时,一次迭代结束。
关于快速排序的讲解,我认为 极客学院 上讲的已经非常好了,可以看一看那上面讲的快速排序。
基于快速排序的思想,可以很快写出用递归的方法实现的快速排序:
itemsize = 2
def _quickSort(indices, start, end):
# start, end are both even
global swap, itemsize
base = indices[start + 1]
i = start + itemsize
j = end
while i != j:
while indices[j+1] >= base and j > i:
j -= itemsize
while indices[i+1] <= base and i < j:
i += itemsize
swap = indices[j:j+itemsize]
indices[j:j+itemsize] = indices[i:i+itemsize]
indices[i:i+itemsize] = swap
# j -= itemsize
# i += itemsize
# else:
# the real value of i is always less than base
if base > indices[i+1]:
swap = indices[start:start + itemsize]
indices[start:start + itemsize] = indices[i:i+itemsize]
indices[i:i+itemsize] = swap
return i
else:
return start
def quickSort(indices, start, end):
if start >= end:
return
i = _quickSort(indices, start, end)
quickSort(indices, start, i-1)
quickSort(indices, i+1, end)
基于函数递归的实现会在实际使用过程中出现栈深度不够的情况,因此需要换用另一种非递归的实现方法:
# 非递归的实现方法
def quickSortIterative(indices, start, end):
global itemsize
stack = [0] * (end - start + 1)
top = -1
top += 1
stack[top] = start
top += 1
stack[top] = end
while top >= 0:
end = stack[top]
top -= 1
start = stack[top]
top -= 1
mid = _quickSort(indices, start, end)
if mid - itemsize > start:
top += 1
stack[top] = start
top += 1
stack[top] = mid - itemsize
if mid + itemsize < end:
top += 1
stack[top] = mid + itemsize
top += 1
stack[top] = end
上面的方法通过一个数组 stack 模拟了栈的行为,在循环体中不断的执行入栈出战操作,这样就不用担心 Python 的函数栈不够用的情况了。
最后用快排的方法在 leetcode 网站上提交的时候,执行时间只用到了 4000ms,比最开始的简单方法快了不少。
参考
https://wiki.jikexueyuan.com/project/easy-learn-algorithm/fast-sort.html
https://www.geeksforgeeks.org/iterative-quick-sort/