算法图解笔记
一、二分查找
1. 仅当列表是有序的时候,二分查找才有用。
2. 对于包含n个元素的有序列表,用二分查找最多需要log2n步(运行时间为对数时间或log时间),而简单查找最多需要n步。
代码:
1 def binary_search(inlist,item): 2 low = 0 # python:from 0 3 high = len(inlist)-1 4 while low<=high: 5 mid = (low + high)/2 6 guess = inlist[mid] 7 if guess == item: 8 return mid 9 if guess <item: 10 low = mid + 1 11 if guess >item: 12 high = mid - 1 13 return None 14 15 my_list = [1,3,5,7,9] 16 print(binary_search(my_list,3)) #output:1 17 print(binary_search(my_list,-1)) #output:None
二、大O表示法
1. 比较的是算法的操作数
2. 并非以秒为单位
3. 算法的速度指的并非是时间,而是操作数的的增速
4. 指出最糟糕情况下的运行时间
从快到慢的顺序
O(logn)<O(n)<O(n*logn)<O(n2)<O(n!) 对应于: 对数时间(二分查找)< 线性时间(简单查找)<(快速排序)<(选择排序)<(商旅问题:遍历n个城市,要求总旅程最小)
三、数组与链表
数组:随机访问、读取数据时有优势、
链表:顺序访问、插入与删除数据时有优势、读取数据时从头开始遍历(读取所有元素时)
数组 | 链表 | |
读取 | O(1) | O(n) |
插入 | O(n) | O(1) |
删除 | O(n) | O(1) |
四、递归
1. 递归由两部分组成:基线条件(函数不再调用自己)和递归条件(函数调用自己)
循环的性能更好,递归让解决方案更清晰,并没有性能上的优势。
2. 栈:“后进先出”,始终在栈顶工作。
3. 调用栈(call stack)即:存储多个函数的变量,描述的是函数之间的调用关系,它由多个栈帧(stack frame)组成,每个栈帧对应着一个未运行完毕的函数,栈帧中保存了该函数的返回地址和局部变量,因而不仅能在执行完毕后找到正确的返回地址,还很自然地保存了不同函数间的局部变量互不相干——因为不同函数对应着不同的栈帧。
调用另一个函数时,当前函数暂停并处于未完成的状态。
4. 使用栈很方便,但是由于其存储详尽的信息(大量的函数调用的信息)会占用大量的内存。解决:改用循环或者尾递归(并非所有语言都支持)。
5. 所有函数的调用都进入调用栈。
五、几种排序算法
性能:直接插入排序 < 选择排序 < 冒泡排序 < 合并排序 < 快速排序
1. 直接插入排序
直接插入排序(Straight Insertion Sort)的基本思想是(两种不同的表述):
1)把n个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。
2)每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。
1 #按照定义 2 #错误代码 3 def Insertsort1(inlist): 4 length = len(inlist) 5 #遍历(1...n-1) 6 for i in range(1,length): 7 j=i-1 8 while j>=0 : 9 if inlist[j]<inlist[i]: 10 break 11 j = j-1 12 #print(j) 13 if j!=i-1: 14 temp = inlist[i] 15 for k in range(i-1,j+1,-1): #error 16 inlist[k+1] = inlist[k] 17 inlist[j+1] = temp 18 return inlist 19 #正确代码 20 def Insertsort2(inlist): 21 length = len(inlist) 22 #遍历(1...n-1)排序(0,1)(0..2)(0..n-1) 23 for i in range(1,length): 24 j=i-1 25 while j>=0 : 26 if inlist[j]<inlist[i]: 27 break 28 j = j-1 29 #print(j) 30 if j!=i-1: 31 temp = inlist[i] 32 for k in range(i-1,j,-1): # right 33 inlist[k+1] = inlist[k] 34 inlist[j+1] = temp 35 return inlist 36 37 my_list = [1,3,2,1,0,7,4] 38 39 res = Insertsort2(my_list) 40 print(res) # [0, 1, 1, 2, 3, 4, 7] 41 #!!! 42 for i in range(2,0,-1): 43 print(i) # 2,1 44 #0不会被输出
2. 冒泡排序
冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序
1 def swap(arr,i,j): 2 arr[i] = arr[i] + arr[j] 3 arr[j] = arr[i] - arr[j] 4 arr[i] = arr[i] - arr[j] 5 return arr 6 #错误代码 7 def selectionSort_3(inarr): 8 for i in range(len(inarr)): 9 for j in range(i,len(inarr)-1): # error 10 if inarr[j]>inarr[j+1]: 11 inarr = swap(inarr,j,j+1) 12 return inarr 13 #正确代码 14 def selectionSort_4(inarr): 15 for i in range(len(inarr)): 16 for j in range(len(inarr)-1-i): 17 if inarr[j]>inarr[j+1]: 18 inarr = swap(inarr,j,j+1) 19 return inarr 20 arr_sort = selectionSort_4([1,3,2,1,0,7,4]) 21 print(arr_sort) # [0, 1, 1, 2, 3, 4, 7] 22 #每一次比较,会将最大的值传到数组的后端!!!
3. 选择排序
选择排序法 是对 定位比较交换法(也就是冒泡排序法) 的一种改进。每趟从待排序的记录中选出关键字最小(或最大)的记录,顺序放在已排序的记录序列末尾,直到全部排序结束为止。依次挑选最小、第二小、第三小。。。
1 def findSmallest(inarr): 2 smallest = inarr[0] 3 smallest_index = 0 4 for i in range(1,len(inarr)): 5 if inarr[i] < smallest: 6 smallest_index = i 7 smallest = inarr[i] 8 return smallest_index 9 # 建立新数组 10 def selectionSort(arr): 11 newArr = [] 12 for i in range(len(arr)): 13 smallest_index = findSmallest(arr) 14 newArr.append(arr.pop(smallest_index)) 15 return newArr 16 17 arr_sort = selectionSort([1,3,2,1,0,7,4]) 18 print(arr_sort) # [0, 1, 1, 2, 3, 4, 7]
1 #交换元素 2 #错误代码: 3 def selectionSort_1(arr): 4 for i in range(len(arr)): 5 smallest_index = findSmallest(arr[i:len(arr)]) 6 if i!= smallest_index: 7 arr[i] = arr[i] + arr[smallest_index] 8 arr[smallest_index] = arr[i] - arr[smallest_index] 9 arr[i] = arr[i] - arr[smallest_index] 10 return arr 11 # smallest_index = findSmallest(arr[i:len(arr)]) 返回的索引是子序列的索引,与arr索引不一样 12 #arr=[1,2,3,4,5],i=1时,findSmallest(arr[1:5])返回的索引为0(2对应的位置),但其在arr处的索引是1。 13 #修改代码 14 def selectionSort_1(arr): 15 for i in range(len(arr)): 16 smallest_index = findSmallest(arr[i:len(arr)]) 17 locate = i + smallest_index #!!! 18 if i!= locate: 19 arr[i] = arr[i] + arr[locate] 20 arr[locate] = arr[i] - arr[locate] 21 arr[i] = arr[i] - arr[locate] 22 return arr 23 24 arr_sort = selectionSort_1([1,3,2,1,0,7,4]) 25 print(arr_sort) # [0, 1, 1, 2, 3, 4, 7]
4. 快速排序
1)选择基准值
2)将数组分成两个子数组:小于基准值的元素和大于基准值的元素
3)对这两个数组进行快速排序(递归调用)
1 def quickSort(inarr): 2 if len(inarr)<2: # 空数组或只含有一个数组的元素 3 return inarr 4 pivot = inarr[0] 5 less = [i for i in inarr[1:] if i <= pivot] 6 greater = [i for i in inarr[1:] if i > pivot] 7 return quickSort(less) + [pivot] + quickSort(greater) 8 9 my_list = [1,3,2,1,0,7,4] 10 print(quickSort(my_list))
5.合并排序(merge sort)
算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
直接插入排序 | 冒泡排序 | 选择排序 | 合并排序 | 快速排序 | |
算法复杂度 | O(n2) | O(n2) | O(n2) | O(nlogn) | O(nlogn) |
算法稳定性 |
注:1. 在大O表示法中n是操作数,用c表示算法所需要的固定时间量,c*n表示算法时间。当两种算法的大O运行时间不同时,这种常量无关紧要。如直接插入排序与快速排序。但对合并排序与快速排序来说,常量的影响很大,由于快速查找的常量比合并查找的常量小,因此运行时间都为O(nlogn),快速查找的时间更快。
2. 快速排序的最糟情况下,运行时间为O(n2)。最佳情况即平均情况,运行时间为O(nlogn)
六、分治算法
1. 分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
2. 分治与递归像一对孪生兄弟,经常同时应用在算法设计之。
3. 分治法适用的情况
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
4. 分治法在每一层递归上都有三个步骤:
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。
七、