01:常用算法
算法其他篇
目录:
- 1.1 常用查找方法
- 1.2 列表排序常用方法介绍
- 1.3 性能最差的三个排序
- 1.4 快排:快速排序中最简单的
- 1.5 堆排序
- 1.6 归并排序(递归调用)
- 1.7 快速排序,堆排序, 归并排序 比较
- 1.8 时间复杂度、空间复杂度和稳定性
算法刷题网站: https://leetcode-cn.com/problemset/all/
1.1 常用查找方法 返回顶部
1、递归
1. 递归条件
1、 自己调用自己
2、 有结束条件
2、二分查找
l = list(range(1,101)) def bin_search(data_set,val): low = 0 high = len(data_set) - 1 while low <= high: mid = (low+high)//2 if data_set[mid] == val: return mid elif data_set[mid] < val: low = mid + 1 else: high = mid - 1 return n = bin_search(l,11) print(n) # 返回结果是: 10
1.2 列表排序常用方法介绍 返回顶部
1、常用排序方法
1、 性能最差的三个排序
1) 冒泡排序
2) 选择排序
3) 插入排序
2、 快速排序
3、 排序NB二人组
1) 堆排序
2) 归并排序
2、时间复杂度
1、循环减半的过程复杂度 O(logn)
2、几次循环就是n的几次方的复杂度
3、排序方法比较
1.3 性能最差的三个排序 返回顶部
1、冒泡排序代码(最好是O(n), 最坏O(n2))
原理:拿自己与上面一个比较,如果上面一个比自己小就将自己和上面一个调换位置,依次再与上面一个比较,第
一轮结束后最上面那个一定是最大的数
import random def bubble_sort(li): for i in range(len(li) - 1): exchange = False for j in range(len(li) - i -1): #内层for循环执行一次,选出一个最大值,将可以调换位置的数调整 if li[j] > li[j + 1]: li[j],li[j+1] = li[j+1],li[j] exchange = True if not exchange: # 如果上一趟没有发生交换就证明已经排序完成 break data = list(range(100)) random.shuffle(data) #将有序列表打乱 bubble_sort(data) print(data)
#! /usr/bin/env pythonf # -*- coding: utf-8 -*- 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[j+1],li[j] li = [1,5,2,6,3,7,4,8,9,0] bubble_sort(li) print(li) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2、选择排序
1、先假定第一个是最小的,依次与其他数比,如果其他数中有比第一个数小就假定这个更小的最小
2、再比,第一轮就可以找到最小的那个放到0号位置,然后在假定1号位置数最小与剩下比较,再找到第二小的数放到第1号位置
import random def select_sort(li): for i in range(len(li) - 1): min_loc = i #开始先假设0号位置的值最小 for j in range(i+1, len(li)): #循环无序区,依次比较,小于min_loc就暂定他的下标最小 if li[j] < li[min_loc]: #所以内层for循环每执行一次就选出一个小值 min_loc = j li[i], li[min_loc] = li[min_loc],li[i] data = list(range(100)) random.shuffle(data) #将有序列表打乱 select_sort(data) print(data)
import random def select_sort(li): for i in range(len(li) - 1): min_loc = i #开始先假设0号位置的值最小 for j in range(i+1, len(li)): #循环无序区,依次比较,小于min_loc就暂定他的下标最小 if li[j] < li[min_loc]: #所以内层for循环每执行一次就选出一个小值 min_loc = j li[i], li[min_loc] = li[min_loc],li[i] li = [1,5,2,6,3,7,4,8,9,0] select_sort(li) print(li) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3、插入排序(比如码牌)
1、列表被分为有序区和无序区两个部分,最初有序区只有一个元素
2、每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空
import random def insert_sort(li): for i in range(1, len(li)): tmp = li[i] #tmp是无序区取出的一个数 j = i - 1 #li[j]是有序区最大的那个数 while j >= 0 and li[j] > tmp: # li[j]是有序区最大的数,tmp是无序区取出的一个数,tmp从有序区最大的那个数开始比 # 小就调换位置,直到找到有序区中值不大于tmp的结束 li[j+1]=li[j] #将有序区最右边的数向右移一个位置 j = j - 1 li[j + 1] = tmp #将tmp放到以前有序区最大数的位置,再依次与前一个数比较 data = list(range(100)) random.shuffle(data) #将有序列表打乱 insert_sort(data) print(data)
1.4 快排:快速排序中最简单的(递归调用) 返回顶部
注:倒序,和 列表中有大量重复元素时,时间复杂度很大
1、快排例子
注:快排代码实现(类似于二叉树 递归调用)----右手左手一个慢动作,右手左手一个慢动作重播
#!/usr/bin/env python # -*- coding:utf-8 -*- import random import sys sys.setrecursionlimit(10000000) #设置系统最大递归深度 def quick_sort(data, left, right): if left < right: mid = partition(data, left, right) # mid返回的是上一个用来排序那个数的下标 quick_sort(data, left, mid - 1) quick_sort(data, mid + 1,right) # 每执行一次partition函数都可以实现将某个数左边都比这个数小右边都比这个数大 def partition(data, left, right): tmp = data[left] while left < right: while left < right and data[right] >= tmp: # 从右向左找小于tmp的数放到左边空位置 right -= 1 data[left] = data[right] # 将右边小于tmp值得数放到左边空位置 while left < right and data[left] <= tmp: # 从左向右找到大于tmp的值放到右边空位置 left += 1 data[right] = data[left] # 将右边大于tmp值得数放到右边空位置 data[left] = tmp return left data = list(range(100)) random.shuffle(data) #将有序列表打乱 quick_sort(data, 0, len(data) - 1) print(data)
#! /usr/bin/env python # -*- coding: utf-8 -*- def quick_sort(arr): ''''' 模拟栈操作实现非递归的快速排序 ''' if len(arr) < 2: return arr stack = [] stack.append(len(arr)-1) stack.append(0) while stack: l = stack.pop() r = stack.pop() index = partition(arr, l, r) if l < index - 1: stack.append(index - 1) stack.append(l) if r > index + 1: stack.append(r) stack.append(index + 1) def partition(arr, start, end): # 分区操作,返回基准线下标 pivot = arr[start] while start < end: while start < end and arr[end] >= pivot: end -= 1 arr[start] = arr[end] while start < end and arr[start] <= pivot: start += 1 arr[end] = arr[start] # 此时start = end arr[start] = pivot return start lst = [1,3,5,7,9,2,4,6,8,10] quick_sort(lst) print lst # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
#! /usr/bin/env python # -*- coding: utf-8 -*- def quick(list): if len(list) < 2: return list tmp = list[0] # 临时变量 可以取随机值 left = [x for x in list[1:] if x <= tmp] # 左列表 right = [x for x in list[1:] if x > tmp] # 右列表 return quick(left) + [tmp] + quick(right) li = [4,3,7,5,8,2] print quick(li) # [2, 3, 4, 5, 7, 8] #### 对[4,3,7,5,8,2]排序 ''' [3, 2] + [4] + [7, 5, 8] # tmp = [4] [2] + [3] + [4] + [7, 5, 8] # tmp = [3] 此时对[3, 2]这个列表进行排序 [2] + [3] + [4] + [5] + [7] + [8] # tmp = [7] 此时对[7, 5, 8]这个列表进行排序 '''
2、快排原理
# 从排序前--------> 到P归位 经历过程(前面都比5小后面都比5大) # 1、 首先从右向左比较,取出列表第一个元素5(第一个位置就空出来)与列表最后一个元素8比较,8>5不换位置 # 2、 用5与-2位置的9比,5<9不换位置 # 3、 5与-3位置的2比较,2<5,将-3位置的5放到1号位置,那么-3号位置空出来了,然后从左往右比较 # 4、 5与2号位置的7比,5<7,将7放到-3号位置,2号位置空出来了,在从右往左比 # 5、 -4号位置的1小于5将1放到空出的2号位置,-4位置空出来了,再从右向左比 # 6、 这样第一次循环就实现了5放到列表中间,前面的都比5大,后面的都比5小
3、快排与冒泡时间复杂度对比
|
最好情况 |
一般情况 |
最坏情况 |
快排 |
O(nlogn) |
O(nlogn) |
O(n^2) |
冒泡 |
O(n) |
O(n^2) |
O(n^2) |
4、快排最坏时间复杂度为何为O(n2)
1. 每次划分只能将序列分为一个元素与其他元素两部分,这时的快速排序退化为冒泡排序
2. 如果用数画出来,得到的将会是一棵单斜树,也就是说所有所有的节点只有左(右)节点的树;平均时间复杂度O(n*logn)
1.5 堆排序 返回顶部
1、堆的定义:http://www.cnblogs.com/MOBIN/p/5374217.html
1、堆中某个节点的值总是不大于或不小于其父节点的值;
2、堆总是一棵完全二叉树
3、完全二叉树定义:
1)若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数
2)第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
4、完全二叉树特性
1)一个高度为h的完全二叉树最多有 2n -1 个节点
2)根为 i 号节点,左孩子 为 2i、 右孩子为 2i+1,父亲节点 (i – 1) / 2
3)一个满二叉树 第 m层节点个数 等于 2m-1 个
4)推倒一个h层的满二叉树为何 有 2h -1 个节点
s = 20 + 21 + 22 + 23 + ...... + 2h-1
s = 21 + 21 + 22 + 23 ...... + 2h-1 - 1
s = 2*21 + 22 + 23 ...... + 2h-1 - 1
s = 22 + 22 + 23 ...... + 2h-1 - 1
s = 2*22 + 23 ...... + 2h-1 - 1
s = 2h -1
2、调长定义(节点的左右子树都是堆但自己不是堆)
1. 调长图解
2. 调长原理
1、首先将2拿出来与9和7比,这里面9最大,就用9作为根
2、2放到9以前的位置,与8和5比,8最大放到开始9的位置
3、2放到起始8的位置与6和4比,6最大,就出现了右边那张图了
3、构造堆:从最后一个有孩子的父亲开始
#! /usr/bin/env python # -*- coding: utf-8 -*- def sift(data, low, high): ''' 构造堆 堆定义:堆中某节点的值总是不大于或不小于父节点的值 :param data: 传入的待排序的列表 :param low: 需要进行排序的那个小堆的根对应的号 :param high: 需要进行排序那个小堆最大的那个号 :return: ''' i = low #i最开始创建堆时是最后一个有孩子的父亲对应根的号 j = 2 * i+ 1 #j子堆左孩子对应的号 tmp = data[i] #tmp是子堆中原本根的值(拿出最高领导) while j <= high: #只要没到子堆的最后(每次向下找一层) #孩子在堆里 # if j < high and data[j] < data[j + 1]: if j + 1 <= high and data[j] < data[j + 1]: #如果有右孩纸,且比左孩子大 j += 1 if tmp < data[j]: #如果孩子还比子堆原有根的值tmp大,就将孩子放到子堆的根 data[i] = data[j] #孩子成为子堆的根 i = j #孩子成为新父亲(向下再找一层) j = 2 * i + 1 #新孩子 (此时如果j<=high证明还有孩,继续找) else: break #如果能干就跳出循环就会流出一个空位 data[i] = tmp #最高领导放到父亲位置 def heap_sort(data): '''调整堆''' n = len(data) # n//2-1 就是最后一个有孩子的父亲那个子堆根的位置 for i in range(n // 2 - 1, -1, -1): #开始位置,结束位置, 步长 这个for循环构建堆 # for循环输出的是: (n // 2 - 1 ) ~ 0 之间的数 sift(data, i , n-1) # i是子堆的根,n-1是堆中最后一个元素 data = [20,50,20,60,70,10,80,30,40] heap_sort(data) print data # [80, 70, 20, 60, 50, 10, 20, 30, 40]
1、在构造有序堆时,我们开始只需要扫描一半的元素(n/2-1 ~ 0)即可,为什么?
2、因为(n/2-1)~0的节点才有子节点,如图1,n=8,(n/2-1) = 3 即3 2 1 0这个四个节点才有子节点
3、所以代码4~6行for循环的作用就是将3 2 1 0这四个节点从下到上,从右到左的与它自己的子节点比较并调整最终形成大顶堆,过程如下:
4、第一次for循环将节点3和它的子节点7 8的元素进行比较,最大者作为父节点(即元素60作为父节点)
5、第二次for循环将节点2和它的子节点5 6的元素进行比较,最大者为父节点(元素80作为父节点)
6、第三次for循环将节点1和它的子节点3 4的元素进行比较,最大者为父节点(元素70作为父节点)
7、第四次for循环将节点0和它的子节点1 2的元素进行比较,最大者为父节点(元素80作为父节点)
注:元素20和元素80交换后,20所在的节点还有子节点,所以还要再和它的子节点5 6的元素进行比较,这就是28行代码 i = j 的原因
8、至此有序堆已经构造好了!如上面右图
4、调整堆
1. 调整堆过程
1、建立堆
2、通过调长,得到堆顶元素,为最大元素
3、去掉堆顶,将最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
4、堆顶元素为第二大元素
5、重复步骤3,直到堆变空
2. 调整堆具体步骤
1、 堆顶元素80和尾40交换后-->调整堆
2、堆顶元素70和尾30交换后-->调整堆
3、堆顶元素60尾元素20交换后-->调整堆
4、其他依次类推,最终已排好序的元素如下
5、堆排序代码实现
# !/usr/bin/env python # -*- coding:utf-8 -*- import random def sift(data, low, high): ''' 构造堆 堆定义:堆中某节点的值总是不大于或不小于父节点的值 :param data: 传入的待排序的列表 :param low: 需要进行排序的那个小堆的根对应的号 :param high: 需要进行排序那个小堆最大的那个号 :return: ''' root = low # root最开始创建堆时是最后一个有孩子的父亲对应根的号 child = 2 * root + 1 # child子堆左孩子对应的号 tmp = data[root] # tmp是子堆中原本根的值(拿出最高领导) while child <= high: # 只要没到子堆的最后(每次向下找一层) #孩子在堆里 if child + 1 <= high and data[child] < data[child + 1]: # 如果有右孩纸,且比左孩子大 child += 1 if tmp < data[child]: # 如果孩子还比子堆原有根的值tmp大,就将孩子放到子堆的根 data[root] = data[child] # 孩子成为子堆的根 root = child # 孩子成为新父亲(向下再找一层) child = 2 * root + 1 # 新孩子 (此时如果child<=high证明还有孩,继续找) else: break # 如果能干就跳出循环就会流出一个空位 data[root] = tmp # 最高领导放到父亲位置 def heap_sort(data): '''调整堆''' n = len(data) ''' n//2-1 就是最后一个有孩子的父亲那个子堆根的位置 ''' for i in range(n // 2 - 1, -1, -1): # 开始位置,结束位置, 步长 这个for循环构建堆 # for循环输出的是: (n // 2 - 1 ) ~ 0 之间的数 sift(data, i, n - 1) # i是子堆的根,n-1是堆中最后一个元素 # 堆建好了,后下面就是挨个出数 for i in range(n - 1, -1, -1): # i指向堆的最后 这个for循环出数然后,调长调整堆 # for循环输出的是 : n-1 ~ 0之间所有的数,n-1就是这个堆最后那个数的位置 data[0], data[i] = data[i], data[0] # 将堆的第一个和最后一个值调换位置(将最大数放到最后) sift(data, 0, i - 1) # 将出数后的部分重新构建堆(调长) data = list(range(100)) random.shuffle(data) # 将有序列表打乱 heap_sort(data) print(data)
6、初始化建堆过程时间:O(n) 公式推倒
参考博客:https://www.cnblogs.com/GHzz/p/9635161.html
说明:建堆时间复杂度指初始化堆需要调整父节点和子节点顺序次数
''' 假设高度为:k ''' #### 1、推倒第i层的总时间:s = 2^( i - 1 ) * ( k - i ) # 说明:如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略; ''' 1. 2^( i - 1):表示该层上有多少个元素 2. ( k - i):表示子树上要下调比较的次数:第一层节点需要调整(h-1)次,最下层非叶子节点需要调整1次。 3. 推倒 倒数第1层下调次数:s = 2^( i - 1 ) * 0 倒数第2层下调次数:s = 2^( i - 1 ) * 1 倒数第3层下调次数:s = 2^( i - 1 ) * 2 倒数第i层下调次数:s = 2^( i - 1 ) * ( k - i ) ''' #### 2、一次新建堆总时间:S = n - longn -1 # 根据1中公式带人推倒 # S = 2^(k-2) * 1 + 2^(k-3)*2.....+2*(k-2)+2^(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1; ''' S = 2^(k-2) * 1 + 2^(k-3)*2.....+2*(k-2)+2^(0)*(k-1) # 等式左右乘上2,然后和原来的等式相减,就变成了: S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1) S = 2^k -k -1 # 又因为k为完全二叉树的深度,所以 (2^(k-1)) <= n < (2^k - 1 ) # 两边同时对2取对数,简单可得 k = logn # 实际计算得到应该是 log(n+1) < k <= logn 综上所述得到:S = n - longn -1,所以时间复杂度为:O(n) '''
7、堆排序时间:O(nlogn) 公式推倒
1)推导方法1:
循环 n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn - logn ;
2)推导方法2:
1. 在一个堆中一次调长(调整堆)时间复杂度: log(n)
2. 排序时一次出栈顶元素需要循环 n次,每次时间复杂度为:log(n)
3. 所以总时间复杂度:nlog(n)
1.6 归并排序(递归调用) 返回顶部
1、归并原理图
2、归并排序代码(时间复杂度:O(nlogn))
#! /usr/bin/env python # -*- coding: utf-8 -*- def merge(li, low, mid, high): ''' :param li: 带排序列表 :param low: 列表中第一个元素下标,一般是:0 :param mid: 列表中间位置下标 :param high: 列表最后位置下标 :return: ''' i = low j = mid + 1 ltmp = [] while i <= mid and j <= high: if li[i] < li[j]: ltmp.append(li[i]) i += 1 else: ltmp.append(li[j]) j += 1 while i <= mid: ltmp.append(li[i]) i += 1 while j <= high: ltmp.append(li[j]) j += 1 li[low:high+1] = ltmp def mergesort(li, low, high): if low < high: mid = (low + high) // 2 #获取列表中间的索引下标 mergesort(li, low, mid) #先分解 mergesort(li, mid+1, high) merge(li, low, mid, high) #然后合并 data = [10,4,6,3,8,2,5,7] mergesort(data, 0 , len(data) -1) print(data) # [2, 4, 6, 8, 10, 12, 14, 16, 18]
1.7 快速排序,堆排序, 归并排序 比较 返回顶部
1、三种排序算法时间复杂度都是( O(nlogn) )
2、 一般情况下,就运行时间而言:
快速排序 < 归并排序 < 堆排序
3、三种排序算法的缺点
1、快速排序: 极端情况下排序效率低( O(n2) )
2、归并排序: 需要额外内存开销(需要新建一个列表放排序的元素)
3、堆排序: 在快的排序算法中相对较慢,堆排序最稳定
1.8 时间复杂度、空间复杂度和稳定性 返回顶部
1、各种算法比较
2、算法不稳定定义
定义:在排序之前,有两个数相等,但是在排序结束之后,它们两个有可能改变顺序.
说明:在一个待排序队列中,A和B相等,且A排在B的前面,而排序之后,A排在了B的后面.这个时候,我们说这种算法是不稳定的.
3、不稳定的几种算法
1)快排为什么不稳定
3 2 2 4 经过第一次快排后结果:2 2 3 4 (第3号位置的2第一次排序后跑到第1号位置了)
2)堆排序为什么不稳定
如果堆顶3先输出,则,第三层的27(最后一个27)跑到堆顶,然后堆稳定,继续输出堆顶,是刚才那个27
这样说明后面的27先于第二个位置的27输出,不稳定
3)选择排序为什么不稳定
5 8 5 2 9 第一次假定1号位置的5最小,但是实际最小的是4号位置的2
第一次排序后为:2 8 5 5 9 以前1号位置的5跑到3号位置5的后面了
作者:学无止境
出处:https://www.cnblogs.com/xiaonq
生活不只是眼前的苟且,还有诗和远方。