目录:
抽象数据类型
数据类型用class来实现,包含属性和方法
属性一般是使用某种特定的数据类型,而方法一般是对属性的操作。
- 实现ADT有三个注意事项
- 如何选用恰当的数据结构作为存储
- 选取的数据结构能否满足ADT的功能需求
- 实现效率如何
线性结构
数组array
python内置了一个array模块,但是大部分人甚至都没用过它
array是内存连续、存储的都是同一数据类型的结构,而且只能存数值和字符
可以看一下array的文档
但是可能很少会使用它,更多的还是使用list
列表list
list其实和C++的STL中的vector类似,可能是我们使用最频繁的数据结构之一
list是python提供的非常基础的数据结构,不需要我们再去实现
如果需要了解底层如何实现,可以看一下cpython解释器的具体实现
需要关注一些的就是list一些操作的时间复杂度:
操作 | 平均时间负责度 |
---|---|
list[index] | O(1) |
list.append | O(1) |
list.pop(index),default last element | O(1) |
list.remove | O(n) |
list.insert | O(n) |
- 注意:
判断一个元素是不是在一个list中,即in操作,也是O(n)
在list头部插入是个相当耗时的操作(需要把后面的元素一个一个挪动位置),但是在结尾append就是O(1)
与他类似又不同的就是下面的链式结构
链式结构
单链表
链式结构内存不连续,需要每个节点保存指向下一个节点的指针
简单的链表节点定义:
class Node(object):
def __init__(self,value,next=None):
self.value = value
self.next = next
然后是单链表的定义:
class LinkedList(object):
""" 链接表 ADT
[root] -> [node0] -> [node1] -> [node2]
"""
时间复杂度:
操作 | 平均时间复杂度 |
---|---|
append(value) | O(1) |
appendleft(value) | O(1) |
find(value) | O(n) |
remove(value) | O(n) |
双链表
每个节点既保存下一个节点的指针,也保存上一个节点的指针
时间复杂度:
操作 | 平均时间复杂度 |
---|---|
append(value) | O(1) |
appendleft(value) | O(1) |
remove(node) | O(1) |
headnode() | O(1) |
tailnode() | O(1) |
可以尝试一道例题👉LeetCode LRU缓存
队列和栈
队列queue
Queue类
先看一下源码的定义
class Queue:
def __init__(self, maxsize=0):
# 设置队列的最大容量
self.maxsize = maxsize
self._init(maxsize)
# 线程锁,互斥变量
self.mutex = threading.Lock()
# 由锁衍生出三个条件变量
self.not_empty = threading.Condition(self.mutex)
self.not_full = threading.Condition(self.mutex)
self.all_tasks_done = threading.Condition(self.mutex)
self.unfinished_tasks = 0
def _init(self, maxsize):
# 初始化底层数据结构
self.queue = deque()
- 可以看出的结论:
- 队列可以设置其容量大小
- 底层结构使用的是 collections.deque() 双端列表的数据结构
里面线程锁啥的这里先不分析
用数组实现队列
要是对Queue不熟悉的话,考试的时候可能就需要自己手写了
队列两个操作pop和push,只要两个下表head和tail就可以了,开辟一个定长list,然后移动head和tail,可以借助求余来实现循环使用
双端队列
collection.deque模块
可以用双端链表实现
栈stack
函数的临时变量是存储在栈区的
算法分析-大O
常用时间复杂度
O | 名称 | 举例 |
---|---|---|
1 | 常量时间 | 一次赋值 |
log n \log n logn | 对数时间 | 折半查找 |
n n n | 线性时间 | 线性查找 |
n log n \log n logn | 对数线性时间 | 快速排序 |
n 2 n^2 n2 | 平方 | 两重循环 |
n 3 n^3 n3 | 立方 | 三重循环 |
2 n 2^n 2n | 指数 | 递归求斐波那契数列 |
n ! n! n! | 阶乘 | 旅行商问题 |
哈希表
dict和set查找速度快,是因为底层是哈希表
- 工作过程
通过一个哈希函数来计算一个元素应该放在数组哪个位置,当然对于一个特定的元素,哈希函数每次计算的下标必须要一样才可以,而且范围不能超过给定的数组长度。
字典dict
底层结构:哈希表
字典中最常用的就是K,V存储经常用作缓存,他的key值是唯一的
内置库 collections.OrderedDict 还保持了 key 的添加顺序
dict的key必须是可哈希的,即不能是list等可变对象
d = dict()
d[(1,2)]=1 # 可行
d[[1,2]]=1 # 不可行
集合set
底层实现:哈希表
集合实际上就是一个dict,只不过把他的value设置成了1
class SetADT(HashTable):
def add(self, key):
# 集合其实就是一个 dict,只不过我们把它的 value 设置成 1
return super(SetADT, self).add(key, True)
- 操作
交集:“&”(python中重载__and__实现)或intersection(需要返回值)
并集:“|”(重载__or__实现)或union
差集:“-”或difference
对称集:“^”或symmetric_difference
frozenset
也是一种集合,但是她的内容是无法变动的
使用场景:用一个可迭代对象初始化她,然后只用来判重等操作
查找
线性查找
对于无需序列,直接遍历
二分查找
折半查找,用于有序序列,可以提高查找效率
当然想法不要局限于在数据里找一个确定的数据,在序列里找一个符合条件的值也是查找
例题:CSP 202303-2 垦田计划
基本排序算法
三个简单的但是时间复杂度却不太理想的排序算法:冒泡、选择、插入
冒泡排序 bubble sort
-
思想
对一个数组进行n-1次迭代,每次比较相邻的两个元素,若前者>后者,就交换 -
冒泡含义
每一轮冒泡的一个最大元素就会通过不断比较和交换相邻元素使他转移到最右边 -
代码
def bubble_sort(seq): # O(n^2), n(n-1)/2 = 1/2(n^2 + n)
n = len(seq)
for i in range(n-1):
print(seq) # 我打印出来让你看清楚每一轮最高、次高、次次高...的小朋友会冒泡到右边
for j in range(n-1-i): # 这里之所以 n-1 还需要 减去 i 是因为每一轮冒泡最大的元素都会冒泡到最后,无需再比较
if seq[j] > seq[j+1]:
seq[j], seq[j+1] = seq[j+1], seq[j]
print(seq)
选择排序
-
思想
找到最小的元素插入迭代的起始位置
即:一个 0 到 n-1 的迭代,每次向后查找选择一个最小的元素 -
代码
def select_sort(seq):
n = len(seq)
for i in range(n-1):
min_idx = i # 我们假设当前下标的元素是最小的
for j in range(i+1, n): # 从 i 的后边开始找到最小的元素,得到它的下标
if seq[j] < seq[min_idx]:
min_idx = j # 一个 j 循环下来之后就找到了最小的元素它的下标
if min_idx != i: # swap
seq[i], seq[min_idx] = seq[min_idx], seq[i]
插入排序
- 思想
每次选下一个元素插入已经排序好的数组中(初始已排序数组只有一个元素) - 代码
def insertion_sort(seq):
n = len(seq)
for i in range(1,n):
value = seq[i]
pos = i
while pos>0 and value<seq[pos-1]:
# 找到能插入的位置(即她前一个位置的数据比她小,后一个比她大)
seq[pos] = seq[pos-1]
pos-=1
seq[pos] = value
高级排序算法
分治法
- 分解原问题为若干子问题,这些子问题是原问题的规模最小的实例
- 解决这些子问题,递归地求解这些子问题。当子问题的规模足够小,就可以直接求解
- 合并这些子问题的解成原问题的解
归并排序
利用分治法解决问题
- 分解:将待排序的 n 个元素分成各包含 n/2 个元素的子序列
- 解决:使用归并排序递归排序两个子序列
- 合并:合并两个已经排序的子序列以产生已排序的答案
def merge_sort(seq):
if len(seq)<=1:
return seq
else:
mid = int(len(seq)/2)
left_half = merge_sort(seq[:mid])
right_half = merge_sort(seq[mid:])
new_seq = merge_sorted_list(left_half,right_half)
# 合并两个数组
return new_seq # 返回一个新数组,不是在原数组上修改
def merge_sorted_list(sorted_a,sorted_b):
length_a,length_b = len(sorted_a),len(sorted_b)
a=b=0
new_sorted_seq = list()
while a<length_a and b<length_b:
if sorted_a[a]<sorted_b[b]:
new_sorted_seq.append(sorted_a[a])
a+=1
else:
new_sorted_seq.append(sorted_b[b])
b+=1
if a<length_a:
new_sorted_seq.extend(sorted_a[a:])
elif b<length_b:
new_sorted_seq.extend(sorted_b[b:])
return new_sorted_seq
- 时间复杂度:O(nlg(n))
快速排序
快排使用的也是分治的思想
- 快排工作过程
- 选择基准值 pivot 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。这个过程称之为 partition
- 对这两个子数组进行快速排序。
- 合并结果
- 代码
def quicksort(array):
size = len(array)
if not array or size < 2: # NOTE: 递归出口,空数组或者只有一个元素的数组都是有序的
return array
pivot_idx = 0
pivot = array[pivot_idx]
less_part = [array[i] for i in range(size) if array[i] <= pivot and pivot_idx != i]
great_part = [array[i] for i in range(size) if array[i] > pivot and pivot_idx != i]
return quicksort(less_part) + [pivot] + quicksort(great_part)
-
缺点:
- 需要额外的存储空间->实现inplace原地排序
- partition操作每次都要两次遍历整个数组->改善
-
优化
实现inplace排序并改善partition操作
def quicksort_inplace(array, beg, end): # 注意这里我们都用左闭右开区间,end 传入 len(array)
if beg < end: # beg == end 的时候递归出口
pivot = partition(array, beg, end)
quicksort_inplace(array, beg, pivot)
quicksort_inplace(array, pivot+1, end)
def partition(array, beg, end):
pivot_index = beg
pivot = array[pivot_index]
left = pivot_index + 1
right = end - 1 # 开区间,最后一个元素位置是 end-1 [0, end-1] or [0: end),括号表示开区间
while True:
# 从左边找到比 pivot 大的
while left <= right and array[left] < pivot:
left += 1
while right >= left and array[right] >= pivot:
right -= 1
if left > right:
break
else:
array[left], array[right] = array[right], array[left]
array[pivot_index], array[right] = array[right], array[pivot_index]
return right # 新的 pivot 位置
- 时间复杂度
在比较理想的情况下,比如数组每次都被 pivot 均分,我们可以得到递归式:T(n) = 2T(n/2) + n
通过递归树,可以发现时间复杂度为O(nlog(n))
排序总结
排序算法 | 最差时间分析 | 平均时间复杂度 | 稳定度 | 空间复杂度 |
---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | 稳定 | O(1) |
选择排序 | O(n^2) | O(n^2) | 不稳定 | O(1) |
插入排序 | O(n^2) | O(n^2) | 稳定 | O(1) |
二叉树排序 | O(n^2) | O(n*log2n) | 不一顶 | O(n) |
快速排序 | O(n^2) | O(n*log2n) | 不稳定 | O(log2n)~O(n) |
堆排序 | O(n*log2n) | O(n*log2n) | 不稳定 | O(1) |