數據結構與算法之查找和排序


必備知識點

時間復雜度

時間復雜度是用來估算算法運行速度的一種方式,通常采用大O表示法。
需要注意以下幾點:

  • 時間復雜度指的不是算法運行的時間,而是算法運行的增速。
  • 時間復雜度是估算,一些非必要的會省略。
  • 通常表示為O(n),其中n為操作數。
    快速判斷時間復雜度的方法:
  • 如果發現循環數減半,那么復雜度就是logn。
  • 有幾層循環就是n的幾次方,不要在意具體循環幾次。

遞歸

遞歸比較容易理解,有以下兩個特征:

  • 調用自身
  • 有 終止條件
    我們可以通過sys模塊來進行查看默認最大執行次數,同時 sys.setrecursionlimit() 也能進行更改,默認是1000次.
#遞歸實現斐波那契數列
def fibnacci(n):
	if n=0 or  n=1:
		return 1
	else:
		return fibnacci(n-1)+fibnacci(n-2)	#這就是遞歸的精髓,把復雜重復的運算抽絲剝繭,每遞歸一次就簡化一次

#斐波那契數列可以用更簡單的方法實現
def fibnacci(n):
	a=b=c=1
	for i in range(2,n+1):
		c=a+b
		a=b
		b=c
	return c

#遞歸實現漢諾塔
def hanoi(n, A, B, C):
    if n > 0:
        hanoi(n-1, A, C, B)
        print('%s->%s' % (A, C))
        hanoi(n-1, B, A, C)

查找

簡單查找

簡單查找就是按順序查找,直到查到指定元素,時間復雜度為O(n)。

二分查找

二分查找是對簡單查找的一種優化,但是操作的只能是有序數組,就是通過中間值和需求數比較,通過比較結果來改變左右范圍。
需要注意的是,不要通過切片改變列表,那樣會加大空間復雜度。
尾遞歸的定義:尾遞歸就是所有遞歸語句都是在函數的最后出現的,正常是相當於循環的復雜度,但是python內沒有優化。

def bin_search(li, val):
    low = 0
    high = len(li)-1
    while low <= high: # 只要候選區不空,繼續循環
        mid = (low + high) // 2
        if li[mid] == val:
            return mid
        elif li[mid] < val:
            low = mid + 1
        else: # li[mid] > val
            high = mid - 1
    return -1
#遞歸實現二分法
lst = [1, 2, 33, 44, 555]
def func(left,right,n):
    if left <= right:
        mid = (right + left) // 2
        if n > lst[mid]:
            left = mid+1
        if n < lst[mid]:
            right = mid - 1
        if n == lst[mid]:
            return( "找到了")
    else:
        return("沒找到")
    return func(left,right,n)
ret = func(0,len(lst)-1,44)
print(ret)
#區別:本方法利用切片,改變原序列表,增加了空間復雜度
lst = [1, 2, 33, 44, 555]
count = 0
def func(lis,n):
    left = 0
    right = len(lis)-1
    global count
    count = count + 1
    print(count)
    if left <= right:
        mid = (right + left) // 2
        if n > lst[mid]:
            return func(lis[mid+1:],n)
        elif n < lst[mid]:
            return func(lis[:mid], n)
        else:
            return( "找到了")
    else:
        return("沒找到")
ret = func(lst,44)

一個小技巧

主要思想為:新建列表作為索引,如果一個數的索引存在,說明這個數也存在.

lis = [2,4,6,7]                        #就是計數排序的反向使用,計數排序會在線性排序模塊里說
n = 3            #查找n
lst = [0,0,0,0,0,0,0,0]   #創建一個元素均為0的列表,元素個數為lis中最大的數字加1
li = [0,0,1,0,1,0,1,1]  #把 lis 中對應的數字值變為1
if li[3] == 1:
    print("存在")
else:
    print("不存在")

排序

排序算法是有穩定和不穩定之分。
穩定的排序就是保證關鍵字相同的元素,排序之后相對位置不變,所謂關鍵字就是排序的憑借,對於數字來說就是大小。
排序算法的關鍵點是分為有序區和無序區兩個區域。

冒泡排序

冒泡排序思路:

  • 比較列表中相鄰的兩個元素,如果前面的比后邊的大,那么交換這兩個數。
  • 這就會導致每一次的冒泡排序都會使有序區增加一個數,無序區減少一個數。
  • 可以認為得到一個排序完畢的列表需要n次或者n-1次。n-1次是因為最后一次不需要進行冒泡了,當然n或n-1的得到的列表是一樣的。
    冒泡排序是穩定的。
#基礎冒泡
def bubble_sort(li):
	for i in range(len(li)-1):	#第一層循環代表處於第幾次冒泡
		for j in range(len(li)-i-1):	#第二層循環代表無序區的范圍
			if li[j]>li[j+1]:
				li[j],li[j+1]=li[k+1],li[j]

如果考慮冒泡的最好情況,也就是冒泡沒有進行到n次的時候就已經不出現j>j+1了,那么排序已經進行完畢。

def bubble_sort(li):
	for i in range(len(li)-1):	#第一層循環代表處於第幾次冒泡
		a= 1
		for j in range(len(li)-i-1):	#第二層循環代表無序區的范圍
			if li[j]>li[j+1]:
				li[j],li[j+1]=li[k+1],li[j]
				a=2
		if a=1:
			break	

選擇排序

選擇排序的思路:

  • 一次遍歷找出最小值,放到第一個位置。
  • 再一次遍歷找最小值,在放到無序區第一個位置。
  • 與冒泡一樣是進行n或n-1次
  • 每次都會讓有序區增加一個元素,無序區減少一個元素。那么進行第i次的時候,它的第一位置的索引就是i。注意是無序區。
    選擇排序是不穩定的,跨索引交換(對比於相鄰)就是不穩定的。
def select_sort(li):
	for i in range(len(li)-1):
		min_pos=i	#第幾次,無序區的第一個位置的索引就為幾減一
		for j in range(i+1,len(li)):
			if li[j]<li[min_pos]:	#min_pos會隨着循環變換值
				min_pos=j
		li[i],li[min_pos]=li[min_pos],li[i]

插入排序

插入排序的思路:

  • 在最開始有序區就把列表的第一個元素就放入有序區(這種有序是相對有序)。
  • 在無序區第一個位置取出一個元素與有序區本來存在的元素進行比較,根據大小插入。
  • 插入排序需要n-1次得出結果。
  • 每次進行插入比較就是一步步的往前進行比較,也就是位置所以要一次次的減1,可能出現位置在最前面也就是插入位置索引為0,也可能是在中間,所以有兩種情況。
def insert_sort(li):
	for i in range(1,len(li)):	#i表示需要進行插入的元素的位置
		j=i-1	#j的初始位置,也就是無序區第一個元素的位置
		while j!=-1 and li[j]>li[i]:#只要能夠與無序區元素進行比較,循環就不停止			
        #跳出循環的情況只有是有序區進行比較的元素沒了,但是跳出循環時與li[j]<=li[i]時執行的語句是一樣的,都是li[j+1]=li[i],所以進行一個合並,減少代碼量
            li[j+1]=li[j]	#進行比較的有序區索引加一
			j-=1	#進行比較元素的索引減一
		li[j+1]=li[i]	#也就是成為0號元素

快速排序

快排思路:

  • 取第一個元素,使元素歸位。
  • 歸位的意義為列表分為兩部分,左邊都比該元素小,右邊都比該元素大。
  • 遞歸,進行多次歸位。
    快速排序的時間復雜度為 nlogn。
def _quick_sort(li,left,right):
	if left<right:	#待排序區域至少有兩個元素,left和right指的是索引
		mid = partition(li,left,right)
		_quick_sort(li,left,mid-1)
		_quick_sort(li,mid+1,right)
		
def quick_sort(li):	#包裝一下,因為循環不能直接遞歸,會非常慢
	_quick_sort(li,0,len[li]-1)
	
def partition(li,left,right):        #此函數的意義是歸位
	tmp=li[left]        #left為第一個元素的索引,也就是需要進行歸位的元素的索引
	while left<right:
        #注意,小的在左邊,大的在右邊
		while li[right]>tmp and left<right:	#當right的值小於tmp是退出
			right-=1    #進行下一個right
		li[left]=li[right]    #把left位置放比tmp小的right
		while li[left]<=tmp and left<right:
			left+=1
		li[right]=li[left]	#把right位置放比tmp大的left
	li[left]=tmp        #把tmp放在left=right時剩下的位置
	return left

在Haskell中只需要6行

quicksort :: (Ord a) =>[a] ->[a]
quicksort [] = []
quicksort (x:xs) = 
    let smallersort = quicksort[a|a<- xs, a <=x]    -- 屬於xs,並且小於x
    let biggersort = quick[a|a<- xs,a>x]
    in smallersort ++ x ++ biggersort

堆排序

知識儲備:

  • 樹是一種數據結構,可以通過遞歸定義。
  • 樹是由n個節點構成的集合
    • 如果n=0,那么是一顆空樹。
    • 如果n>0,那么存在 一個根節點,其他的節點都可以單拎出來作為一個樹。
  • 根節點,就是回溯后存在的節點。
  • 葉子節點,就是沒有下層節點的節點。
  • 樹的深度可以粗略認為就是節點最多的分支有幾個節點
  • 度,度就是一個節點由幾個分支,也就是說有幾個子節點
  • 父節點和子節點索引的關系,若父節點的索引為i,左子節點索引為2i+1,右子節點的索引為2i+2,子節點找父節點,i=(n-1)//2
  • 二叉樹:度最多有兩個的樹。
  • 滿二叉樹:一個二叉樹,每一層的節點數都是最大值,也就是2,那么它就是滿二叉樹。
  • 完全二叉樹:葉子節點只能出現在最下層和次下層,並且最下面一層的節點都集中在該層最左側,滿二叉樹是一種特殊的完全二叉樹。

二叉樹的存儲方式:

  • 鏈式存儲,在之后的數據結構博客介紹。
  • 順序存儲,順序存儲就是列表。結構就是從上到下從左到右。
  • 堆:堆是一顆完全二叉樹,分為大根堆和小根堆:
    • 大根堆就是任何的子節點都比父節點小。
    • 小根堆就是任何一個子節點都比父節點大。
  • 堆的向下調整性質:當根節點的左右子樹都是堆,那么就可以將其通過向下調整來建立一個堆
  • 向下調整就是把根節點單獨拿出來,讓它子節點盡行大小比較,然后把根節點插入到子節點位置,子節點成為新的根節點,如此遞歸,直到滿足堆的定義。

堆排序也就是優先隊列,進行多次向下調整,得出一個根堆,然后根據索引從后往前挨個輸出節點。
向下調整

def sift(li,low,high):
	i=low	#相對根節點
	j=2*i+1		#它的左子節點位置
	tmp=li[i]	#根節點元素大小
	while j<=high:
		if j<high and li[j]<li[j+1]:	#先判斷左右節點大小,j<high是因為可能出現沒有有節點的情況
			j+=1
		if tmp <li[j]:		#再判斷左節點或有節點與根節點的大小
			li[i]=li[j]		#把左右節點移動到根節點
			i=j				#把相對根節點移動到下一層
			j=2*i+1			#新的子節點索引
		else:
			break
	li[i]=tmp				#最后把原來的根節點放到索引i上

從堆中取出節點元素

def heap_sort(li):
	for low in range(len(li)//2-1,-1,-1):	#構造堆,low的取值是倒序,從后面到0
		sift(li,low,len(li)-1)		#high被假設是固定的,因為它為最小對結果不會影響。
	for high in range(len(li)-1,-1,-1):	#取出時high一直是動態的,讓取出的low不參加之后的調整,也就是構建新堆的過程
		li[0],li[high]=li[high],li[0]	#把得出的無序區最后一個值,放到根結點處進行構建新堆
		sift(li,0,high-1)	#

python的heapq內置模塊

  • nlargest,nsmallest 前幾個最大或最小的數
  • heapify(li) 構造堆
  • heappush 向堆里提交然后構造堆
  • heappop 彈出最后一個元素

歸並排序

歸並排序思路:

  • 一次歸並:含義就是給定一個列表,分為兩段有序,讓它成為一個整體有序的列表。
  • 一次歸並的方法:把兩段有序列表,兩兩比較,把較小的那個元素拿出來,若一方元素數量為0,那么就將另一方所有元素取出。
  • 歸並排序: 先分解后合並,分解為單個元素,那么單個元素就是有序的,然后再兩兩一次歸並,得到有序列表。
  • 也就是把歸並排序看成多次的一次歸並。
def merge(li, low, mid, high):	#mid為兩段有序分界線左邊第一個數的索引
    # 列表兩段有序: [low, mid] [mid+1, high]
    i = low		#i指向左半邊列表進行比較的元素
    j = mid + 1	#j指向右半邊列表進行比較的元素
    li_tmp = []	#比較出較小的元素暫存的位置
    while i <= mid and j <= high:	#當左右兩側比較的元素都不為空時
        if li[i] <= li[j]:	#左邊小,左邊元素拿到暫存li_tmp中,左邊指針向右移動
            li_tmp.append(li[i])
            i += 1
        else:				#右邊小,右邊元素拿到li_tmp中,右邊元素的指針向左移動
            li_tmp.append(li[j])
            j += 1
    while i <= mid:		#將剩余的元素都拿到li_tmp中
        li_tmp.append(li[i])
        i += 1
    while j <= high:
        li_tmp.append(li[j])
        j += 1
    for i in range(low, high+1):	#把li_tmp中的元素放到li中
        li[i] = li_tmp[i-low]
    # 也可以這樣移動: li[low:high+1] = li_tmp

def _merge_sort(li, low, high): #排序li從low到high的范圍
    if low < high:
        mid = (low + high) // 2	#開始遞歸分散
        _merge_sort(li, low, mid)
        _merge_sort(li, mid+1, high)
        merge(li, low, mid, high)	#合並

希爾排序

希爾排序思路:

  • 希爾排序是一種分組插入排序算法,可以理解為是對插入排序的優化。
  • 首先去一個整數d1=n/2,將元素分為d1個組,每組相鄰兩個元素之間的距離為d1,在各組內直接插入排序。
  • 接下來,去第二個元素d2=d1/2,重復上一步,直到di=1。
  • 即所有元素在同一組內進行直接插入排序。
  • 希爾排序每次都會讓列表更加接近有序,在那過程中不會使某些元素變得有序。
def shell_sort(li):
	gap = len(li) // 2
	while gap > 0:    #接下來進行的其實就是插入排序的代碼,只不過無序區起始是從gap也就是從gap開始。
		for i in range(gap, len(li)):
			tmp = li[i]
			j = i - gap
			while j >= 0 and tmp < li[j]:
				li[j + gap] = li[j]
				j -= gap
			li[j + gap] = tmp
		gap /= 2

線性時間排序

線性時間排序都有局限性,並不常用。

計數排序

計數排序思路:

  • 計數排序時間復雜度為O(n),是一種特殊的桶排序。
  • 首先必須知道列表中數的范圍
  • 加入最大值的大小為d,那么就建立一個元素數目為d的值均為0的列表,這也是計數排序的局限性,如果最大數很大的化,計數排序會很占用內存。
  • 遍歷原列表,如果值為t的某個數出現了一次,那么就在新建列表中索引為t的元素的值上加1.
  • 最后再遍歷新建列表,把索引作為值,值作為出現次數依次列出。
def count_sort(li,max_num):
    count = [0 for _ in range(max_num+1)]        #列表表達式,之前的博客有寫,和Haskell中一樣_代表我們完全不再在乎它的值
    for i in li:
        count[i]+=1        #出現一次,值加一
    li = []
    for i,v in enumerate(count):        #i是元素,v是出現的次數
        for _ in range(v):
            li.append(i)

桶排序

桶排序思路:

  • 桶排序是對計數排序的一種改造,時間復雜度O(nk)
  • 計數排序可以看成每個桶里只裝一個元素的桶排序。
  • 首先根據待排序列表的數據分布進行分桶,桶是有順序且不間斷的。
  • 然后分別對桶里的元素進行排序
  • 桶內元素排序完成之后,整體列表也就排序成功了
  • 桶內排序可以采用任何算法
  • 內嵌排序會由於桶排序而降低時間復雜度,也就是以空間換時間,乘法變加法。
  • 得根據實際情況寫代碼,例子就不寫了,思路很簡單。

基數排序

要說基數排序,先說多關鍵字排序。
多關鍵字排序思路:

  • 多關鍵字排序就是,在XX條件的基礎上進行排序。
  • 比如日常場景中的,對薪資相等的員工,由年齡從大到小進行排序。
  • 排序的方式為 先有年齡從大到小進行排序,再對薪資進行排序。
  • 其中年齡被稱為低關鍵字,薪資為高關鍵字,也就是說,先行條件為高,先行條件的基礎上的條件為低。
  • 原因是穩定排序,大小相等的元素,其相對位置不變。
    有了這個思路,再說基數排序:
  • 基數排序是針對整數的。
  • 可以思考兩位數的大小排序,個位數為低關鍵字,十位數就是高關鍵字。
  • 基數排序就是把個位數0-9分成10個桶,依次進行排序。
  • 因為桶的索引是從0-9,那么只要放進桶里就相當於已經完成了一次排序

預備知識:

  • 如何獲取一個整數的個位數、十位數,百位數。。。
    • 首先,得出列表中的最大值,除以10取余可以獲得個位數
    • 其次,除以10取整,會舍去個位數,變成以原十位作為個位的數
    • 思路就是不斷的取整,取余
def get_dight(num,i):
    #i為1時,取個位數,i為2時取十位數
    return num//(10 * i)%10

還可以轉成列表后根據索引得出,思路和上面是一樣的,就當科普了。

#int轉list
def int2list(num):
    li=[]
    while num>0:
        li.append(num%10)
        num //=10
    li.reverse_list()    #再科普一個列表反轉吧
    return li

列表反轉思路:
- 把列表分為左右兩邊
- 對應的元素交換位置

def reverse_list(li):
    n=len(li)
    for i in range(n//2):
        li[i],li[n-i-1] = li[n-i-1],li[i]
    return li
  • 說完列表反轉再說一個int反轉吧
    • 整數有正負之分
    • 首先求出num的余數a,num地板除得到b。
    • a*10+b,得到后兩位數的反轉
    • 重復上述兩步,直到地板除為0。
def reverse_int(num):
    is_neg = False
    if num < 0:
        is_neg = True
        num = -1 * num
    res = 0
    while num >0:
        res = res * 10
        res = res + num%10
        num = num // 10
    if is_neg:
        res = res * -1
    return res

。。
直接上基數排序:

def max_nu(li):
    for i in range(len(li)-1):
        max_pos=i
        for j in range(i+1,len(li)-1):
            if li[i] < li[j]:
                max_pos=j
def radix_sort(li):
    max_num=max_nu(li)
    i = 0                                      #從個位數開始,
    while (10 ** i <= max_num):    #這里注意是可以 = 的,要考慮 10 100 的情況
        buckets = [[] for i in range(0,10)]
        for val in li:
            digit = li // (10 ** i)%10
            buckets[digit].append[val]    #以余數作為索引,放入桶內,第一次循環是個位數,第二次循環是十位數
        li = []
        for bucket in buckets:
            for val in bucket:        #在桶中依次取出數據
                li.append(val)        #進行一次排序后放回li中
        i+=1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM