算法小结(一)

递归特点:调用自身,有结束条件。

注意区分下面两种递归:

 1 def func(x):
 2     if x > 0:
 3         print(x)
 4         func(x - 1)  
 5  func(5)输出 5 4 3 2 1
 6 
 7 def func2(x):
 8     if x > 0:
 9         func2(x - 1)
10         print(x)     
11  func(5)输出 1 2 3 4 5
递归区分

 

时间复杂度
是一个单位,也是一个估计值,这也就表示下面的代码:

 1 print(x)
 2 print(x)
 3 print(x)      
 4 
 5 不是O(3) 而是 O(1)
 6 
 7 for i in range(n):
 8     print(x)
 9     for j in range(n):
10         print(x)
11 
12 不是O(N^2 + N) 而是 O(N^2)
13 
14 for i in range(n):
15     for j in range(i):
16         print(x)
17 
18 不是O(N^2 / 2)而是 O(N^2)
19 
20 而这段代码:
21 while i > 0:
22     print(i)
23     i = i // 2  
24 
25 他的时间复杂度为O(logN),假设输入64 则输出 64 32 16 8 4 2 ,即 6 次,则2的6次方为64,即log2 64 = 6(以2为底64的对数)
时间复杂度

 

如何一眼粗略的看出复杂度?
1. 是否有循环减半操作,减半就logN
2. 几层循环就N的几次方

空间复杂度,用来评估算法内存占用大小的一个式子。
用几个变量的时候就是O(1),用一个列表的时候就是O(N)。

列表查找分为,
顺序查找
二分查找

tips:不要在一个递归函数上加装饰器,如果想加装饰器,则需要新开一个函数,加上装饰器,然后返回递归函数

tips:尾递归(return 递归函数)的运行速度跟正常函数一样。

tips:切片为什么慢?
因为它要重新再创建一个列表或字符串来保存切好的。

###二分查找
二分查的前提条件:有序列表,然后设立两个指针 low 和 high,得到他们的中间值 mid ,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。

def binary_search(li, target):
    low = 0
    high = len(li) - 1
    while low <= high:
        mid = (low + high) // 2
        if li[mid] == target:
            return mid
        elif li[mid] > target:
            high = mid - 1
        elif li[mid] < target:
            low = mid + 1
    return
二分查找

 

排序:
首先看 LowB 三人组,冒泡,选择,插入。

冒泡排序
按升序排时,列表每两个相邻的数,如果前边的比后边的大,则交换着两个数。

一些名词:
一趟:表示的是当交换到最后,把最大的数交换上去的过程。趟数从0开始。
有序区,已经有序的区域,
无序区,还无序的区域。

冒泡的趟数为 N - 1,N 是数组长度,要减一的原因是当冒泡排到最后一个时,数组的最后一个也自然的有序了(肯定是最小的),而趟数就是最外层的循环。

趟数里的操作次数,假设趟数为 i ,则趟数操作次数为 N - i - 1,因为每完成一趟,则需要操作的数就减少一个,而交换时,不用交换有序区(因为交换是两个数的操作),所以减一。而趟数里的操作就是内层循环。

def bubble_sort(li):
    # 这里长度不用减一的原因是,range操作取不到最后,就相当于帮我们减一了。
    n = len(li) 
    for i in range(n - 1):
        for j in range(n - i - 1):
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]

因为有两层循环,所以时间复杂度为O(N^2)
冒泡排序

 

优化:如果冒泡排序中执行一趟而没有发生交换,则表明列表已经是有序的了,可以直接结束算法。可以通过设置一个 flag,通过 flag 来判断是否发生了交换。

 1 def bubble_sort(li):
 2     for i in range(len(li) - 1):
 3         flag = False
 4         for j in range(len(li) - i - 1):
 5             if li[j] > li[j + 1]:
 6                 li[j], li[j + 1] = li[j + 1], li[j]
 7                 flag = True
 8         if not flag:
 9             break
10 
11 但是时间复杂度还是O(N^2),因为还是两层循环,不过当遇到最好情况(已经排序好),时间复杂度是O(1)
冒泡排序优化

 

选择排序
一趟遍历选出最小的数,放第一个位置,再一趟遍历选出剩余列表最小的,周而复始。

 1 def select_sort(li):
 2     n = len(li)
 3     for i in range(n - 1):
 4         # 最开始假定下标为i 的元素是最小的
 5         min_loc = i
 6         # 因为设定 i 为最小的,所以range时从 i + 1 开始,就不用重复判断i 是否是最小的
 7         for j in range(i + 1, n - 1):
 8             if li[j] < li[min_loc]:
 9                 min_loc = j
10         # 当一趟执行完后,找到最小的元素的下标,然后和下标为 i 的进行交换
11         li[i], li[min_loc] = li[min_loc], li[i]
12 
13 复杂度 O(N^2)
选择排序

 

插入排序
列表被分为有序区和无序区,最初有序区只有一个元素,每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。(可以想象打牌时抓牌的场景)。

 1 def insert_sort(li):
 2     n = len(li)
 3     # 最初有序区有一个元素,所以从 1 开始, i 代表第几次来排序,也表示排的第几个元素,因为每次就排好一个元素
 4     for i in (1, n):
 5         tmp = li[i]
 6         # 这个需要排序的来和他前面位置的比较大小
 7         j = i - 1
 8         while j >= 0 and li[j] > tmp:
 9             # 右移过程
10             li[j + 1] = li[j]
11             j -= 1
12         # 因为右移过程会伴随的 j - 1 的操作,所以这里想将tmp存到合适位置j要 j + 1 
13         li[j + 1] = tmp
14 
15 复杂度O(N^2)
插入排序

 

快速排序
取一个元素p(一般是第一个),使元素p归位(到他应该的位置),并使列表分为两部分,左边都比p小,右边都比p大。然后递归完成排序。

 1 def quick_sort(li, left, right):
 2     # 递归终止条件,不等于的原因是有两个元素才需要进行排序
 3     if left < right:
 4         # 使元素归位,mid 是放置好的元素的下标
 5         mid = partition(li, left, right)
 6         quick_sort(li, left, mid - 1)
 7         quick_sort(li, mid + 1, right)
 8 
 9 def partition(li, left, right):
10     刚开始将最左边的数存起来,这样最左边就出现空位了,然后准备动右边的指针
11     tmp = li[left]
12     while left < right:
13         # 当右边的数比这个存起来的左边数大,继续动右边指针,因为我们最终目的是想要右边的数大,左边数小
14         while left < right and li[right] >= tmp:
15             right -= 1
16         # 当出现右边数比左边数小的情况,则交换这两个数,然后交换后,右边出现空位,开始动左边指针
17         li[left] = li[right]
18         while left < right and li[left] <= tmp:
19             left += 1
20         li[right] = li[left]
21     # 运行到这里,left 已经等于 right 了,而且还剩一个空位,就是tmp的应该到的位置
22     li[left] = tmp
23     return left
24 
25 复杂度,NlogN,复杂度原因,不严谨的假设有64个元素,
26 64
27 第一遍分为了 32 32
28 32
29 第二遍分为了 16 16
30 16
31 第三遍分为了 8 8
32 8
33 第四遍分为了 4 4
34 4
35 第五遍分为了 2 2
36 2
37 第六遍分为了 1 1
38 
39 所以就有6次partition(logN),然后一次partition其实数组被遍历了一遍,所以为NlogN
40 
41 tips:Python系统内置的sort,非常快,但是其实时间复杂度跟快排是一个数量级(NlogN),那他快在哪?
42 因为它是用C写的,所以比我们手写的快排快。所以还是老老实实的用系统自带的sort吧。
快速排序

 

快排的问题:
1. 最差情况

        最好情况    一般情况    最差情况
快排      O(NlogN)    O(NlogN)    O(N^2)[倒序的时候,每次partition都只减少了一次而不是分成两半]
冒泡        O(N)      O(N^2)      O(N^2)

 

2. 递归最大深度限制
当递归次数超过一定范围,程序就挂了,所以需要设置递归最大深度

import sys
sys.setrecursionlimit(N) # N 为设置的最大深度

堆排序
进化流程:树 -> 二叉树(度不超过2的树) -> 完全二叉树 -> 大根堆 / 小根堆

概念
- 根节点,叶子节点
- 树的深度(高度)[层数]
- 树的度[最大的节点的分叉数]
- 孩子节点 / 父节点
- 子树


二叉树存储方式:
1. 链式存储方式
2. 顺序存储方式(列表)[我们下面用的]


 

父节点和左孩子节点的编号下标有什么关系?
i ~ 2i + 1 当父节点为i 时,左孩子节点为2i + 1
父节点和右孩子节点的编号下标有什么关系?
i ~ 2i + 2
所以,可以通过以上规律从父亲找到孩子或者从孩子找到父亲

堆,一颗特殊的完全二叉树,分为大根堆和小根堆
大根堆:满足任一节点都比其孩子节点大
大根堆

小根堆:满足任一节点都比其孩子节点小
小根堆


当根节点的左右子树都是堆,但自身不是堆的时候,可以通过一次向下的调整来将其变换成一个堆。

堆排序过程:
1. 建立堆
2. 得到堆顶元素,为最大元素(这里用的大根堆)
3. 去掉堆顶,将堆的最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
4. 堆顶元素为第二大元素
5. 重复步骤3,直到堆变空

 1 # low ~ high 范围的小堆,low 是堆顶
 2 def shif(li, low, high):
 3     # i 永远指向的是根节点
 4     i = low 
 5     j = 2 * i + 1
 6     tmp = li[i]
 7     while j <= high:
 8         # 如果有右孩子(j<high)且右孩子比左孩子大,则让j指向右孩子,即j 永远指向最大的孩子
 9         if j < high and li[j] < li[j+1]:
10             j += 1
11         # 当孩子比存的根节点大时,将孩子移至根节点位置,然后重置i (i = j, i 永远指向的是根节点,其实堆的最后的数也能算节点,只是他们的分支为空), 让他成为新的根节点,j成为以他为根节点的左孩子,继续判断他是否有资格做这个新的根节点。
12         if li[j] > tmp:
13             li[i] = li[j]
14             i = j
15             j = 2 * i + 1
16         # 当当前范围的根节点是最大时,跳出循环
17         else:
18             break
19     # 最后将存储的tmp 放到调整完后的节点位置。
20     li[i] = tmp
一次调整

 

 1 def heap_sort(li):
 2     n = len(li)
 3     # 最后一个有孩子的父亲的下标,数学证明出来的,记住即可
 4     last_father_index = n // 2 - 1
 5     # 循环顺序是从最后一个有孩子的父亲 -> 堆顶,倒着循环的。第二个参数为 -1 是因为range顾头不顾尾,想要取到堆顶,也就是index为0的位置,就要把他设为 -1
 6     # 注意这里不要被这个循环迷惑,这个循环作用就是找到根节点,以便下面的调整操作
 7     for i in range(last_father_index, -1, -1):
 8         # 这里可以永远写 n - 1(堆的最后一个)
 9         sift(li, i, n - 1)
10     #############  这样一个堆就建立好了 #############
11     
12     for i in range(n - 1, -1, -1):
13          li[0], li[i] = li[i], li[0]
14          sift(li, 0, i - 1)
15     ############# 以上操作就是去掉堆顶,将堆的最后一个元素放到堆顶,
16     # 只是这里为了节省新开列表的空间,将要出来的堆顶存到了原来最后一个元素的位置,然后在调整时,通过改变high的范围来减小堆的范围,而且,由于他将最大的存在最后面,所以最后排出的列表是升序的。########
17 
18     li_new = []
19     for i in range(n - 1, -1, -1):
20         li_new.append(li[0])
21         li[0] = li[i]
22         sift(li, 0, n - 1)
23     return li_new 
24     ############ 这是第二种写法,比较好理解,就是新开一个列表,来存通过依次出数处理的堆顶元素,
25     # 但是要注意的是他对原列表不会排序,而是返回一个已经排序好的新列表。 ##########
26 
27 复杂度为O(NlogN)
堆排序

 

归并排序
归并:将两段有序的列表合成为一个有序列表的过程。

 1 # low: 第一段有序列表的开头
 2 # mid: 第一段有序列表的结尾,所以mid + 1 就为第二段有序列表的开头
 3 # high: 第二段有序列表的结尾
 4 def merge(li, low, mid, high):
 5     i = low
 6     j = mid + 1
 7     ltmp = []
 8     # 两边都必须有数,这样才能比较两边数的大小
 9     while i <= mid and j <= high:
10         if li[i] < li[j]:
11             ltmp.append(li[i])
12             i += 1
13         else:
14             ltmp.append(li[j])
15             j += 1
16     # 比到最后,肯定有一段列表有剩余,则将剩余全部添加进新列表
17     while i <= mid:
18         ltmp.append(li[i])
19         i += 1
20     while j <= mid:
21         ltmp.append(li[j])
22         j += 1
23     # 这里用切片不会减慢速度。因为它在等号左边。
24     li[low:high + 1] = ltmp
一次归并

 

 1 # 因为这里是先调用merge_sort递归,再进行merge操作,
 2 # 所以就会将列表越分越小,直至分成一个元素,而一个元素时就是有序的。
 3 # 这样merge操作的条件就满足了。然后通过merge将他们合并。
 4 # 总结下来就是先分解,后合并。
 5 def merge_sort(li, low, high):
 6     if low < high:
 7         mid = (low + high) // 2
 8         merge_sort(li, low, mid)
 9         merge_sort(li, mid + 1, high)
10         merge(li, low, mid, high)
11 
12 时间复杂度:O(nlogn)
13 空间复杂度:O(n)
归并排序

 

快速排序、堆排序、归并排序-小结

三种排序算法的时间复杂度都是O(nlogn)

一般情况下,就运行时间而言:
快速排序 < 归并排序 < 堆排序

三种排序算法的缺点:
- 快速排序:极端情况下排序效率低
- 归并排序:需要额外的内存开销
- 堆排序:在快的排序算法中相对较慢

希尔排序
希尔排序是一种分组插入排序算法

希尔排序每趟并不使得某些元素有序,而是使整体数据越来越接近有序,最后一趟使得所有数据有序。

 1 def shell_sort(li):
 2     # gap表示分组的间隔
 3     gap = len(li) // 2
 4     while gap >= 1:
 5         for i in range(gap, len(li)):
 6             tmp = li[i]
 7             # j表示每组的前一张牌,因为每组都是间隔为gap
 8             j = i - gap
 9             while j >= 0 and tmp < li[j]:
10                 li[j+gap] = li[j]
11                 j -= gap
12             li[i-gap] = tmp
13         gap /= 2
14 
15 时间复杂度:  O((1+τ)n)    τ:tuo , 0 < τ < 1
16                 一般认为是O(1.3n),比O(NlogN)慢
希尔排序
posted @ 2018-02-26 11:24  Zoulf  阅读(317)  评论(0)    收藏  举报