題目:
給定一個整數數組 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 的函數棧不夠用的情況了。
排序后進行查找
排完序后(數組中的值從小到大排列),找出指定大小的兩數之和 Target 就非常簡單了:
- 首先用最小的數 A,與另一個數較大的數 B (B 從最大的數開始嘗試)相加,比較 A + B 與 Target 的大小,若 A + B < Target,則 B 取較小的一個數,繼續比較 A + B 與 Target,否則當 A + B > Target 時,執行第 2 步
- 固定 B,可以利用 “二分法” 查找合適的 A,若 B 之前的數都無法滿足 A + B == Target 時,B 再取較小的一個數,繼續進行第 2 步
最后用快排的方法在 leetcode 網站上提交的時候,執行時間只用到了 4000ms,比最開始的簡單方法快了不少。
解題的完整代碼地址: github
參考
https://wiki.jikexueyuan.com/project/easy-learn-algorithm/fast-sort.html
https://www.geeksforgeeks.org/iterative-quick-sort/