数据结构与算法
一、排序算法
Chiglish
author: Beilop Jiao
email: 781933206@qq.com
Time: 2020-11-17 13:44
1、binary_search(O(log n))
这个算法的思想类似于猜数游戏:现在我们需要从1到9里猜出来预设好的一个数字。一般我们不会直接从1猜到9,这样如果目标数字是9,我们需要猜9次。
如果我们从5开始猜,显然第一次猜小了,下一次我们猜7,以此类推,只需要最多4次就可以猜到我们需要的。
可以看到,我们每次把数字分成了两部分,每次都会直接抛掉一半数字而从另一半数字里去猜。这样我们只需要log2n次就可以找到目标数字。
def binary_search(list,item):
low = 0
high = len(item)-1 #定位我们猜的数的位置,即mid是几
while low <= high:
mid = (low+high)/2
guess = list[mid]
if guess == item: #猜对了
return mid
if guess > mid: #猜小了
high = mid - 1
else: #猜大了
low = mid + 1
return None #如果数字列表里没有我们想猜的那个,返回None
2、选择排序(O(n2))
例如,按个子从小到大排队,每次浏览一遍队伍,然后选出最小的站前面,然后再浏览一次,选第二小的站在第二个位置,以此类推。
#先定义一个找最小元素的函数
def findSmallest(arr):
smallest = arr[0] #存最小值
smallest_index = 0 #存最小值位置
for i in range(1,len(arr)):
if smallest > arr[i]: #开始遍历,如果当前值小于最小值,则把最小值替换为当前值
smallest = arr[i]
smallest_index = i
return smallest_index
#然后开始写选择排序
def selectionSort(arr):
newArr = []
for i in range(len(arr)):
smallest = findSmallest(arr)
newArr.append(arr.pop(smallest)) #pop参数是index,会删除原数组对应索引的值,然后返回被删除的值
return newArr
3、快速排序(O取决于基准值,平均O(nlog n),最大O(n2))
选基准值,找出小于基准值的数字和大于的数字,构成两个子数组,对子数组再进行快排。调用栈高度为O(log n),每层用时O(n)。所以总体是O(nlog n)。
快速排序比合并排序运行所需时间常数小。所以在快排平均状态使用快排而非合并排序。
def quicksort(arr):
if len(arr)<2:
return arr #基准条件,为空或长度为一是有序的
else:
pivot = arr[0] #递归条件
less = [i for i in arr[1:] if i<pivot] #小于基准值子数组
greater = [i for i in arr[1:] if i>pivot] #大于基准值子数组
return quicksort(less)+ [pivot]+ quicksort(greater) #拼接
4、广度优先算法(O(V(vertice顶点)+E(edge边数)))
解决是否有路径和哪个是最短路径问题。
def search(name):
search_queue = deque()
search_queue +=graph[name]
searched = [] #记录检查过的人
while search_queue: #只要队列不为空
person = search_queue.popleft() #就弹出左边第一个
if not person in searched: #如果没检查过
if person_is_seller(person): #判断是否芒果销售商
print('he is')
return True
else:
search_queue +=graph[person]#不是则把他的邻居也加入搜索队列
searched.append(person) #标记此人已检查
return False
def person_is_seller(name):
return name[0]== '芒' #如果首字母是芒,则是芒果销售商
5、深度优先算法
6、狄克斯特拉算法
二、数据结构
1、数组(读O(1)写O(n)删O(n ))
在数组里的数据在内存里都是连着的,如果这块内存已满,加入数据需要重新申请内存地址。类似带朋友看电影,如果附近都坐满了,但是新来了一位朋友,你们就需要换个能坐下的地方。
所以添加新数据会很慢,我们一般用“预留座位”的方法来处理。但这样也会带来问题;
1.空间浪费
2.超出预留空间,仍需要重新申请
数组里的元素必须相同。
2、链表(读O(n)写O(1)删O(1))
链表内数据可存储在内存任何地方,链表每个元素存储了下个元素地址,从而让一系列随机内存地址串在一起。类似带明显提示的寻宝游戏,每个地点都有下个地点的信息:下个地点在五号楼203房间。
所以添加元素只需要把当前元素放入内存,并把其地址存储在前一个元素里。
链表的优势在于插入数据,缺点在于访问数据:需要访问最后一个元素时,必须先访问前一个元素。
需要跳读数据时,链表效率低,数组效率高。但是需要读取所有数据时,链表效率高。
3、链表数组
组合使用数组和链表可以实现很多其他适合我们应用场景的数据结构。例如存储用户名的时候,直接存储在数组或者链表里都会遇到狠多问题,读取和写入都是常见的操作。一个简单的操作是用包含26个字母的数组去存储首字母元素,然后数组每个元素指向一个链表。假设创建Beilop用户,首先访问B所在的数组,然后把此用户名添加在对应链表尾部。
这种方式的读写的速度是介于链表和数组间的。
4、递归
这很像复杂性科学里的自指,就是自己调用自己。如果没有看过前目的地这部科幻电影的可以看一下,将会很好的理解这个概念。
使用递归,我们将可能更容易理解程序;使用循环则可能有更高性能。
写递归的时候,必须写基线条件(base case不再自我调用)和递归条件(recursive case自己调用自己)。
递归使用了调用栈(见6),不光存储下一步的调用,还要存储未完成的调用。
递归思想:分而治之(D&C divide and conquer)
(1)找出基线条件,必须尽可能简单。
(2)不断分解问题(缩小规模),直到符合基线条件。
5、堆
6、栈(LIFO)
类似待办事项清单,每次只读取最上面一条信息,办完删掉(弹出);或者在最上面加入一条信息(压入)。
在函数里调用别的函数的时候,当前函数暂停并处于未完成状态。
调用栈(call stack):存储上述这些多个函数变量的就是栈就叫调用栈。
使用栈很方便,但也导致存储详尽信息可能会占用大量内存。每个函数调用都占一定内存,如果栈太高,意味着计算机存储了大量函数调用信息,有以下两个解决办法:
1、重写代码,改用循环
2、使用尾递归,这是个高级递归,暂不讨论。另外并非所有语言支持尾递归。
7、散列表
散列函数会把输入映射到数字。需要满足以下需求:
**1、输出必须一致。**例如输入apple得到4,每次输入都得是4。
2、它应该将不同的的输入映射到不同的数字。
散列表:散列函数+数组,用散列函数确定元素存储位置。也称为散列映射、映射、字典。
a = {}
def dict_1(name):
if a.get(name):
print('exist')
else:
a[name] = True
print('yes')
应用:1、查找(查找网址);2、防重复(投票);3、缓存(服务器存储网页,下次省略搜索过程,直接调给用户)
冲突
当散列表给两个键分配相同位置的时候,叫做冲突。例如按字母表排序分配内存的散列表,apple在第一个,banana在第二个,再分配avocado时又是第一个位置。
这个时候,需要解决冲突。最简单的办法是:如果两个键映射到同一位置,在此位置存一个链表。
性能(平均O(1)最糟(O(n)))
如果你只卖a打头商品,你上述的散列表就会把所有元素存在列表第一个位置的一个长链表里,速度会很慢。所以我们知道散列函数需要均匀地把键分配到散列表不同位置,而链表长度尽可能短。
为了避免冲突,提高性能,需要:
1、较低的填装因子
2、良好的散列函数
填装因子= 散 列 表 包 含 的 元 素 数 位 置 总 数 \frac {散列表包含的元素数}{位置总数} 位置总数散列表包含的元素数
填装因子越低,越不容易冲突。不错的经验是填装因子大于0.7就调整散列表长度。
良好的散列函数
==良好的散列函数让数组里的值呈均匀分布。==不需要了解什么样的是良好的,如果好奇,可以研究SHA函数。
7、队列(FIFO)
只有入队、出队两种操作,是先进先出的,而栈是后进先出。