前言
本人是一個長期的數據分析愛好者,最近半年的時間的在網上學習了很多關於python、數據分析、數據挖掘以及項目管理相關的課程和知識,但是在學習的過程中,過於追求課程數量的增長,長時間關注於學習了多少多少門課程。事實上,學完一門課之後真正掌握的知識並不多,主要的原因是自己沒有認真學習和理解溫故而知新的這句話的真正含義。因此,從現在開始,我在學習《數據結構與算法——基於python》的課程內容之後,抽出固定的時間對每天學習的內容進行總結和分享,一方面有助於個人更好的掌握課程的內容,另一方面能和大家一起分享個人的學習歷程和相應的學習知識。
第五節 二分搜索
1 順序查找:
找到目標的第一個位置,如果找不到則返回-1;
程序
def bi_search_iter(alist, item):
left, right = 0, len(alist) - 1
while left <= right:
mid = (left + right) // 2
if alist[mid] < item:
left = mid + 1
elif alist[mid] > item:
right = mid - 1
else: # alist[mid] = item
return mid
return -1
num_list = [1,2,3,5,7,8,9]
print(bi_search_iter(num_list, 7))
print(bi_search_iter(num_list, 4))
輸出結果
2 二分查找模板:
在剛開始學習寫程序的時候,按照下面程序的思路體現四個重點內容,就可以解決百分之五十的二分查找難題;
程序
def binarysearch(alist, item):
# 第一個重點
if len(alist) == 0:
return -1 # 注意邊界條件
# 在碰到類似的情況時,將邊界條件程序寫上去;對運行時間沒有影響;
# 可以減少後續擔心的情況;
left, right = 0, len(alist) - 1
# 第二個重點
while left + 1 < right: # 當L和R相鄰的時候,當L和R相等、R小於L的時候,跳出循環體;
mid = left + (right - left) // 2
# 第三個重點
if alist[mid] == item:
right = mid # 找到的首次出現的目標元素的位置;
elif alist[mid] < item:
left = mid
elif alist[mid] > item:
right = mid
# 第四個重點
# 退出循環之後,需要進行兜底的判斷;
# 當L和R相鄰,當L和R相等的時候,L和R都可能等於2,所以將目標元素和L和R相比,因爲找第一個位置,所以將L放在前面 ;兜底
if alist[left] == item:
return left
if alist[right] == item:
return right
return -1
3 在旋轉有序數列中查找最小值
假設有一個升序排列的數列在某個未知節點處被前後調換,請找到數列中的最小值;
程序
# 方法1
def searchlazy(alist): #O(nlgn)
alist.sort() # 先排序
return alist[0]# 返回第一個元素
# 方法2
def searchslow(alist):#O(n)
mmin = alist[0]
for i in alist:
mmin = min(mmin, i)
return mmin
# 方法3
def search(alist):# O(lgn)
if len(alist) == 0:
return -1 # 第一個重點,先寫邊界條件
# 第二個重點
left, right = 0, len(alist) - 1
while left + 1 < right:
if (alist[left] < alist[right]):# 如果數據有序,未被旋轉;
return alist[left];
# 第三個重點
mid = left + (right - left) // 2
if (alist[mid] >= alist[left]):# 前半部分有序,到後半部分去尋找
left = mid + 1
else:
right = mid # 到前半部分去找
# 第四個重點;兜底 L和R相鄰,
return alist[left] if alist[left] < alist[right] else alist[right]
4 在旋轉數組中查找
假設有一個升序排列的數列在某個未知節點處被前後調換,請找到目標值;
程序
#O(lgn)
def search(alist, target):
if len(alist) == 0: # 第一個重點:初始條件的判斷
return -1
left, right = 0, len(alist) - 1
while left + 1 < right:
mid = left + (right - left) // 2
# 第二個重點:
if alist[mid] == target:
return mid
# 第三個重點:
if (alist[left] < alist[mid]):
if alist[left] <= target and target <= alist[mid]: # 判斷target是否在前半部分
right = mid # 在前半部分尋找
else:
left = mid # 在後半部分尋找
else: #與上面相反
if alist[mid] <= target and target <= alist[right]:
left = mid
else:
right = mid
# 第四個重點:兜底程序
if alist[left] == target:
return left
if alist[right] == target:
return right
return -1
5 搜索插入位置
• 給定有序數組和一個目標值,如果在數組中找到此目標值則返回目標值的index,如果 沒有找到,則返回目標值按順序應該被插入的位置index.
• 注:可以假設數組中不存在重複數
程序
# 首先找到第一個大於等於target的數
def search_insert_position(alist, target):
if len(alist) == 0: # 第一個重點
return 0
left, right = 0, len(alist) - 1
while left + 1 < right: #第二個重點
mid = left + (right - left) // 2
if alist[mid] == target:
return mid
# 第三個重點
if (alist[mid] < target):
left = mid
else:
right = mid
# 第四個重點:兜底
if alist[left] >= target:
return left
if alist[right] >= target:
return right
# 當L和R都小於target ,插入在最後;
return right + 1
6 搜索區間
• 給一個有序、有重複數字的數組。找到給定目標值的開始和結束位置,如果目標數據不存在,返回(-1,-1)
思路
思路
首先運行找到第一個元素的程序,然後運行找到最後一個元素的程序;時間複雜度 O(lgn)
程序
# O(lgn)
def search_range(alist, target): # 第一個重點,base
if len(alist) == 0:
return (-1, -1)
lbound, rbound = -1, -1
# search for left bound
left, right = 0, len(alist) - 1
while left + 1 < right: # 第二個重點
mid = left + (right - left) // 2
if alist[mid] == target: # 第三個重點
right = mid
elif (alist[mid] < target):
left = mid
else:
right = mid
if alist[left] == target: # 第四個重點 兜底程序
lbound = left
elif alist[right] == target:
lbound = right
else:
return (-1, -1)
# search for right bound
left, right = 0, len(alist) - 1
while left + 1 < right:
mid = left + (right - left) // 2 # 第二個重點
if alist[mid] == target: # 第三個重點
left = mid
elif (alist[mid] < target):
left = mid
else:
right = mid
if alist[right] == target: # 第四個重點
rbound = right
elif alist[left] == target:
rbound = left
else:
return (-1, -1)
return (lbound, rbound)
7 在用空字符串隔的字符串的有序數列中查找
給定一個有序的字符串序列,這個序列中的字符串用空字符隔開,請寫出找到給定字符串位置的方法;
思路
先從左往右或者從右往左找到非字符元素,然後再按照二分法,尋找mid,再將target和mid和尋找的非字符運輸做比較,再決定mid向哪一端移動。
程序
# O(n) 可以用in逐項遍歷
def search_empty(alist, target):
if len(alist) == 0:
return -1
left, right = 0, len(alist) - 1
while left + 1 < right:
while left + 1 < right and alist[right] == "": # 從右邊開始找,找到第一個非空字符串,並將其位賦值給right
right -= 1
if alist[right] == "":
right -= 1
if right < left:
return -1
mid = left + (right - left) // 2
while alist[mid] == "": # 當中間是空字符串時,向後尋找;
mid += 1
if alist[mid] == target:
return mid
if alist[mid] < target:
left = mid + 1
else:
right = mid - 1
if alist[left] == target:
return left
if alist[right] == target:
return right
return -1
8 在無限序列中找到某元素的第一個出現位置
有序數據流
不知道序列長度
思路
先從左往右或者從右往左找到非字符元素,然後再按照二分法,尋找mid,再將target和mid和尋找的非字符運輸做比較,再決定mid向哪一端移動。
程序
# 在有很多0的數組中找1出現的位置
def search_first(alist):
left, right = 0, 1
while alist[right] == 0:
left = right
right *= 2 # right變爲2倍
if (right > len(alist)):
right = len(alist) - 1
break
return left + search_range(alist[left:right+1], 1)[0]
9 **供暖設備 **
• 冬季來臨!你的首要任務是設計一款有固定供暖半徑的供暖設備來給所有的房屋供 暖。
• 現在你知道所有房屋以及供暖設備在同一水平線上的位置分佈,請找到能給所有房 屋供暖的供暖設備的最小供暖半徑。
• 你的輸入是每個房屋及每個供暖設備的位置,輸出應該是供暖設備的最小半徑
思路
轉換爲兩個數組,尋找一個數組在另外一個數組中距離最近的值。
先將供暖設備的數組進行排序;
找到每個房屋左邊和右邊距離最近的供暖設備,再選出最近的距離和對應的設備。
在所有房屋的最近距離的最大值。
程序
from bisect import bisect
def findRadius(houses, heaters):
heaters.sort() #對設備進行排序
ans = 0
for h in houses:
hi = bisect(heaters, h)# bisect:找到heaters第一個大於等於h的位置,也是h應該在heaters插入的位置,但是不插入
left = heaters[hi-1] if hi - 1 >= 0 else float('-inf') # 查找左邊最近的設備的位置
right = heaters[hi] if hi < len(heaters) else float('inf') # 查找右邊最近設備的位置
ans = max(ans, min(h - left, right - h)) # 返回所有距離最小值得最大值
return ans
houses = [1,12,23,34]
heaters = [12,24]
findRadius(houses, heaters)
輸出結果
10 sqrt(x)
計算並返回x的平方根。x保證是一個非負整數。
程序
方法1
# 二分查找法
def sqrt(x):
if x == 0:
return 0
left, right = 1, x
while left <= right:
mid = left + (right - left) // 2
if (mid == x // mid):
return mid
if (mid < x // mid):
left = mid + 1
else:
right = mid - 1
return right
sqrt(40)
輸出結果
方法2
def sqrtNewton(x):
r = x
while r*r > x:
r = (r + x//r) // 2
return r
sqrtNewton(125348)
輸出結果
11 矩陣搜索
在一個MN的矩陣裏,每一行都是排好序的,每一列也是排好序的,請設計一個算法在矩陣中查找一個數。
思路
1、依次遍歷矩陣中的數組; O(MN)
2、在每一行(列)分別採用二分法查找;min(nlgm、mlgn)
3、因爲矩陣的每一行每一列都是排好序的,所有每個元素都大於其左邊和上面的元素,小於右邊和下面的元素;但是對於邊界元素,當元素和其相比之後,根據相比的結果就只有一個方向進行移動。
程序
**12 矩陣搜索 II **
在一個MN的矩陣裏,每一行都是排好序的,每一列也是排好序的,請設計一個算法在矩陣中查找第K大的數。
思路
1、依次遍歷矩陣中的數組; O(MN)
2、在每一行(列)分別採用二分法查找;min(nlgm、mlgn)
3、因爲矩陣的每一行每一列都是排好序的,所有每個元素都大於其左邊和上面的元素,小於右邊和下面的元素;但是對於邊界元素,當元素和其相比之後,根據相比的結果就只有一個方向進行移動。
**13 找到重複數 **
給定一個包含n+1個整數的數組,其中每個元素爲1到n閉區間的整數值,請證明至少 存在一個重複數。假設只有一個重複數,請找到這個重複數。
要求:
1、不能太慢;
2、不給排序;
3、不準用set!
4、不準對原數據進行修改;
思路
數據的範圍爲1到n,找到中間值m,將所有的數字和m比較,從而得知多少數字小於m多少大於m,然後可以判斷出重複的數字在前面還是後邊。如果在前面,再找出1到m的中間值m1,如果在前面,再找出m到n的中間值m2,依次類推,就能找到重複數;
時間複雜度:nlogn;
程序
# O(nlogn)
def findDuplicate(nums):
low = 1
high = len(nums)-1
while low < high: # 大循環
mid = low + (high - low) // 2
count = 0
for i in nums: # 找到中間值m,將所有的數字和m比較。
if i <= mid:
count+=1
if count <= mid:
low = mid+1
else:
high = mid
return low
nums = [3,5,6,3,1,4,2]
findDuplicate(nums)
輸出結果
14 地板和雞蛋
假設有一個100層高的建築,如果一個雞蛋從第N層或者高於N層墜落,會摔破。如果 雞蛋從任何低於N層的樓層墜落,則不會破。現在給你2個雞蛋,請在摔破最少雞蛋 的情況下找到N。
思路
第一個雞蛋找出範圍。第二個雞蛋精確定位;
爲充分利用雞蛋和平均分配;
①如果第1個雞蛋在第k層摔破了,第二個雞蛋就可以從第1層開始慢慢測試,最多k次可以測試到準確樓層;
②如果第1個雞蛋在k層沒有摔破,這個時候就只剩下k-1次機會了,第2次測試的時候第1個雞蛋就可以在第k+(k-1)層測試。如果第2次第1個雞蛋摔破了,第2個雞蛋就可以從k層開始慢慢的往k+(k-1)層測試,如果沒有摔破,就繼續同樣的往更高層測試,第三次的話就應該是k+(k-1)+(k-2)層測試了,這樣就可以確保剩下的機會可以準確測試到摔破的樓層。
③所以公式是:k+(k-1)+(k-2)+…+1>=100;轉化一下就是:k(k+1)/2>=100;求解k>=14;所以100層樓最少14次可以測試到準確摔破樓層;
15 找到兩個有序數組的中值
給定兩個長度分別爲N1和N2的有序數組,請用時間複雜度爲 O(log N) 的方法找到所 有元素的中值,這裏N=N1+N2。。
思路
16 合併區間
• 給定一個區間的集合,將所有存在交叉範圍的區間進行合併。
• 輸入: [[1,3],[2,6],[8,10],[15,18]]
• 輸出: [[1,6],[8,10],[15,18]]
• 說明: 因爲區間 [1,3] 和 [2,6] 存在交叉範圍, 所以將他們合併爲[1,6].
思路
首先根據每個區間的起始時間進行排序;得到三種結果:每個區間首尾都沒有交叉、區間之間有交叉、區間之間有包含;對於區間有交叉和包含的情況,新區間的開始時間爲開始時間的最小值,結束時間爲結束時間的最大值;
程序
class Interval:
def __init__(self, s=0, e=0):
self.start = s
self.end = e
def __str__(self):
return "[" + self.start + "," + self.end + "]"
def __repr__(self):
return "[%s, %s]" % (self.start, self.end)
def merge(intervals):
intervals.sort(key=lambda x: x.start) # 對所有的起始時間進行排序
merged = []
for interval in intervals:
# if the list of merged intervals is empty or if the current
# interval does not overlap with the previous, simply append it.
if not merged or merged[-1].end < interval.start:
merged.append(interval)# 這種情況說明沒有交叉
else:
# otherwise, there is overlap, so we merge the current and previous
# intervals.
merged[-1].end = max(merged[-1].end, interval.end) # 該種情況說明有交叉,需要合併
return merged
i1 = Interval(1,3)
i2 = Interval(2,6)
i3 = Interval(8,10)
i4 = Interval(15,18)
intervals = [i1,i2,i3,i4]
print(merge(intervals))
輸出結果
17 插入區間
• 給定一個沒有交叉範圍的區間集合,在這個集合中插入一個新的區間(如果需要,請進行合併)。
• 你可以認爲這些區間已經初始時根據他們的頭元素進行過排序
• 輸入:區間集合=[[1,3],[6,9]], 新區間 = [2,5]
• 輸出:[[1,5],[6,9]]
思路
先插入區間;再合併;
新進入的區間有三種情況;和原有區間沒有交叉,和原有區間有交叉;
對於有交叉的情況,先合併,再輸出;
程序
def insert(intervals, newInterval):
merged = []
for i in intervals:
if newInterval is None or i.end < newInterval.start:
merged += i,
elif i.start > newInterval.end:
merged += newInterval,
merged += i,
newInterval = None
else:
newInterval.start = min(newInterval.start, i.start)
newInterval.end = max(newInterval.end, i.end)
if newInterval is not None:
merged += newInterval,
return merged
i1 = Interval(1,3)
i2 = Interval(6,9)
intervals = [i1,i2]
new = Interval(2,5)
insert(intervals, new)
輸出結果