目录
一、算法的引入
1.1 感受算法(demo)
写一个demo,如果 a+b+c=1000,且 a^2+b^2=c^2(a,b,c 为自然数),如何求出所有a、b、c可能的组合?(最初的想法是枚举法)
# 如果 a+b+c=1000,且 a^2+b^2=c^2(a,b,c 为自然数),如何求出所有a、b、c可能的组合?
# 第一步 分析需求
# 找到所有满足以上两个条件的a,b,c的组合
# 第二步 设计算法 # 枚举法
# 尝试a,b,c的所有组合,判断当前的组合是否满足以上两个条件,如果满足,就输出,否则就尝试下一个组合
# 第三步 代码实现
import time
start_time = time.time() # 开始时间
for a in range(0, 1001):
for b in range(0, 1001):
for c in range(0, 1001):
'''
c = 1000 - a - b # (算法的优化,c就不需要循坏,优化法)
if a**2 + b**2 == c**2
'''
if a + b + c == 1000 and a**2 + b**2 == c**2:
print(f'组合{a},{b},{c}满足条件')
end_time = time.time() # 结束时间
print('程序执行的时间为:', end_time-start_time, '秒')
算法的特点:
- 输入项: 算法具有0个或多个输入
- 输出项: 算法至少有1个输出
- 有穷性: 算法必须能在有限个步骤之后终止,并且需要在可接受的时间内
- 确切性: 算法的每一步骤必须有确切的定义
- 可行性: 算法的每一步都是可行的
判断一个算法的好坏主要是通过看时间复杂度
区别:时间频度和时间复杂度的概念
时间频度:一个算法中的语句执行次数称为语句频度或时间频度, 记为T(n)
时间复杂度的概念:全称渐进时间复杂度,描述随着问题的数据规模的增长,算法的时间频度的增长趋势.记作O(F(n)),F(n)是T(n)的渐进函数.
根据上面的小测试,计算时间频度 T 时间复杂度 # 程序运行的时间不一样,但是运行的步骤是一样的 # 程序运行的步骤 时间频度 T # T = 1000 * 1000 * 8 # T = 1000 * 1000 * 3 # t = 2000 * 2000 * 3 # 如果说把问题的数据规模设为n,T=n*n*3 # T = 3*n^2 # 当n为无穷大时,时间频度的式子中,谁的值最大,那么时间复杂度就是谁 # O(n^2)# T = 3*n^3 + 2*n^2 + 10000 n^3 + n^2 # O(n^3)时间复杂度的计算方法:
- 计算时,往往只关注时间频度中的最高次项,其他次要项和常数项忽略
- 顺序结构,时间复杂度按加法来计算
(例如:让用户输入两个列表,一个列表的长度是m,另一个长度是n,对这两个列表分别求和,比较它们的和的大小,循环遍历,分别求和,比较大小,m步,n步 O(m+n))
- 循环结构,时间复杂度按乘法来计算
- 分支结构,时间复杂度取最大值
- 没有特殊说明时,算法的时间复杂度都是指最坏的时间复杂度
1.2 常见的时间复杂度
1.3 列表时间复杂度和字典时间复杂度的比较
1.4 认识数据结构和算法的关系
什么是数据结构?
数据结构 是计算机存储、组织数据的方式. 数据结构 将数据以某种关系组织在一起。
算法是基于数据结构去设计的 也可以说算法的第一步就是选择数据结构
程序 = 数据结构 + 算法
二、顺序表
1.1 认识顺序表和内存地址的关系
问题:1, 2, 3, 4, 5这样一组数据如何以某种关系存储在计算机中 使得只要知道其中一个元素的地址 就可以得到其他元素的地址?
位(bit) 最小的存储单位,每一位存储一个1位的二进制码
字节(byte) 由8个bit组成的存储单元
常见数据类型的内存大小
1、整数类型:
int (整数类型)= 4个字节 = 32bit
float(浮点数)=4个字节(单精度浮点数)/8个字节(双精度浮点数)
2、布尔类型:
bool = 1字节 (但实际上,一个字节是最小的可寻址内存单元,因此通常只能以字节为单位)
3、字符串
char(字符)=1字节
wchar_t
(宽字符)=2字节或4字节(取决于平台)4、数组和列表
list
(列表):在Python中,列表的内存大小是动态变化的,取决于列表中包含的元素数量和元素类型。array
(数组):在其他语言中,数组的内存大小也是根据元素类型和数量动态计算的。5、字符串
str
(字符串):在Python中,字符串的内存大小也是动态变化的,取决于字符串的长度和编码方式6、字典和集合
dict
(字典):在Python中,字典的内存大小也是动态变化的,取决于键值对的数量和键值的类型。
set
(集合):在Python中,集合的内存大小也是动态变化的,取决于集合中元素的数量和元素的类型7、自定义对象
内存大小取决于对象的成员变量和其类型。在Python中,可以使用
sys.getsizeof()
函数来获取对象占用的内存大小
1.2 顺序表
1.3 顺序表的扩充机制
一体式顺序表: 表头和数据区存储在一起,扩容时表头也要跟着变化
分离式顺序表: 表头和数据区分开存储,扩容时表头不需要变化(列表)
1.4 列表底层逻辑的实现
lst = []
"""
列表对象
表头 容量 元素个数
数据区
lst.allocated 容量
lst.size 元素个数
lst.items = 数据区
"""
lst.append(1)
PY_SSIZE_T_MAX = float('inf') #列表最大的容量
obj_size = 1
class List:
allocated = 0
size = 0
items = []
def list_resize(self, new_size):
"""
self: 列表对象本身
:param new_size: 修改之后的元素个数
:return:
"""
allocated = self.allocated # 获取列表对象当前的容量
# allocated >> 1 ==> allocated // 2
if allocated >= new_size >= (allocated >> 1):
self.size = new_size
return 0
# 计算需要的内存容量是多少
new_allocated = new_size + (new_size >> 3) + (3 if new_size < 9 else 6)
if new_allocated > PY_SSIZE_T_MAX:
return -1
if new_size == 0:
new_allocated = 0
# 计算我们容量需要的字节数
num_allocated_bytes = new_allocated * obj_size
# 获取到新的内存空间的地址
items = addr(self.items, num_allocated_bytes)
if items == None:
return -1
# 让列表对象的数据区地址指向新的内存空间的地址
self.items = items
self.size = new_size
self.allocated = new_allocated
return 0
三、链表
3.1 链表概念
链表是一种物理存储单元上非连续、非顺序的存储结构 数据元素的逻辑顺序通过链表中的指针链接次序实现.
链表由一系列结点组成,结点可以在运行时动态生成, 每个结点包括两个部分:存储数据元素的数据域、存储下一个节点地址的指针域
3.2 单向链表:
3.2.1 单向链表的实现:
3.2.2 添加(add)头部节点图解
3.2.3 插入insert实现思路
3.2.4 append尾部加入数据
3.2.5 remove 删除节点
class Node:
# 节点类
def __init__(self, data, _next=None):
self.data = data # 数据域
self.next = _next # 指针域
class SingleLinkList:
def __init__(self):
self.head = None # 链表的头结点
self._length = 0 # 链表的长度,链表的元素个数
def is_empty(self):
# 判断链表是否为空
return self._length == 0
def length(self):
# 返回链表的长度
return self._length
def nodes_list(self):
# 返回链表中的所有节点的值组成的列表
res = []
cur = self.head
while cur:
res.append(cur.data)
cur = cur.next
return res
def add(self, data):
# 往链表的头部添加一个节点,值为data
# 新建一个节点node
node = Node(data)
# 先让node指向当前链表中的头结点
node.next = self.head
# 再让链表的head指向当前node节点
self.head = node
# 添加节点之后,链表的长度加1
self._length += 1
def append(self, data):
# 往链表的尾部添加一个节点,值为data
# 新建一个节点node,值为data
node = Node(data)
# 找到链表的尾节点
# 从头结点开始,遍历链表中的所有节点
# 每次判断当前节点的next是否为空
# 为空说明当前节点就是尾节点
# 不为空 通过当前节点的next去访问下一个节点,
if self.head:
cur = self.head
while cur.next:
cur = cur.next
# 让当前的尾节点的指针域指向node
cur.next = node
else:
self.head = node
# 链表的长度+1
self._length += 1
def insert(self, pos, data):
# 往链表的指定位置插入一个节点,值为data
if pos <= 0:
self.add(data)
elif pos > self._length:
self.append(data)
else:
# 正常的输入
# 第一步 新建一个节点 node
node = Node(data)
# 第二步
cur = self.head
while pos - 1:
cur = cur.next
pos -= 1
# 到这里之后,cur指向的是索引为pos-1的节点
# 让node的next指向索引为pos的节点
node.next = cur.next
# 让索引为pos-1的节点的next指向cur
cur.next = node
self._length += 1
def remove(self, data):
# 删除链表中第一个值为data的结点
cur = self.head
prev = None # 要删除的节点的前驱结点
while cur:
if cur.data == data:
# 如果前驱结点为空,说明我们要删除的节点是第一个节点
if not prev:
self.head = cur.next
else:
prev.next = cur.next
self._length -= 1
return 0
prev = cur
cur = cur.next
return -1
def modify(self, pos, data):
# 修改链表中指定位置节点的值
if 0 <= pos < self._length:
cur = self.head
while pos:
cur = cur.next
pos -= 1
cur.data = data
else:
print('你输入的范围不符合要求')
def search(self, data):
# 查找链表中是否有节点的值为data
cur = self.head
while cur:
if cur.data == data:
return True
cur = cur.next
return False
if __name__ == '__main__':
l1 = SingleLinkList() # 新建一个链表类
print(l1.head, l1.length())
l1.add(1)
print(l1.head.data, l1.length())
l1.add(3)
print(l1.head.data, l1.length())
print(l1.nodes_list())
l1.append(4)
print(l1.head.data, l1.length())
print(l1.nodes_list())
l1.insert(7, 7)
print(l1.nodes_list())
print(l1.search(11))
3.3单向循环链表
3.3.1单向循环链表的实现
class Node:
# 节点类
def __init__(self, data, _next=None):
self.data = data # 数据域
self.next = _next # 指针域
class SingleCycleLinkList:
def __init__(self):
self.head = None # 链表的头结点
self._length = 0 # 链表的长度,链表的元素个数
self.tail = None # 链表的尾节点
def is_empty(self):
# 判断链表是否为空
return self._length == 0
def length(self):
# 返回链表的长度
return self._length
def nodes_list(self):
# 返回链表中的所有节点的值组成的列表
res = []
if self.is_empty():
return res
res.append(self.head.data)
cur = self.head.next
while cur != self.head:
res.append(cur.data)
cur = cur.next
return res
def add(self, data):
# 往链表的头部添加一个节点,值为data
# 新建一个节点node
node = Node(data)
if self.is_empty():
self.head = node
node.next = self.head
else:
# 先让node指向当前链表中的头结点
node.next = self.head
# 让链表的尾节点的next指向Node
cur = self.head
while cur.next != self.head:
cur = cur.next
cur.next = node
# 再让链表的head指向当前node节点
self.head = node
# 添加节点之后,链表的长度加1
self._length += 1
def append(self, data):
# 往链表的尾部添加一个节点,值为data
# 新建一个节点node,值为data
node = Node(data)
# 找到链表的尾节点
# 从头结点开始,遍历链表中的所有节点
# 每次判断当前节点的next是否为空
# 为空说明当前节点就是尾节点
# 不为空 通过当前节点的next去访问下一个节点,
if self.head:
cur = self.head
while cur.next != self.head:
cur = cur.next
cur.next = node # 原本的尾节点指向新建的节点
else:
self.head = node
node.next = self.head # 新的尾节点指向当前的头结点
# 链表的长度+1
self._length += 1
def insert(self, pos, data):
# 往链表的指定位置插入一个节点,值为data
if pos <= 0:
self.add(data)
elif pos > self._length:
self.append(data)
else:
# 正常的输入
# 第一步 新建一个节点 node
node = Node(data)
# 第二步
cur = self.head
while pos - 1:
cur = cur.next
pos -= 1
# 到这里之后,cur指向的是索引为pos-1的节点
# 让node的next指向索引为pos的节点
node.next = cur.next
# 让索引为pos-1的节点的next指向cur
cur.next = node
self._length += 1
def remove(self, data):
# 判断一下链表是否为空,为空那必然没有值为data的节点
if self.is_empty():
return -1
# 删除链表中第一个值为data的结点
cur = self.head
flag = True # 标志位的作用是,让第一次循环能进入
prev = None # 要删除的节点的前驱结点
while cur != self.head or flag:
flag = False # 让循环继续的条件就必须是cur != self.head
if cur.data == data:
# 如果前驱结点为空,说明我们要删除的节点是第一个节点
if not prev:
# 找到尾节点
last_node = self.head
while last_node.next != self.head:
last_node = last_node.next
# 让尾节点的next指向新的head
last_node.next = self.head.next
self.head = cur.next # self.head.next
else:
prev.next = cur.next
self._length -= 1
return 0
prev = cur
cur = cur.next
return -1
def modify(self, pos, data):
# 修改链表中指定位置节点的值
if 0 <= pos < self._length:
cur = self.head
while pos:
cur = cur.next
pos -= 1
cur.data = data
else:
print('你输入的范围不符合要求')
def search(self, data):
# 查找链表中是否有节点的值为data
if self.is_empty():
return False
cur = self.head
flag = True
while cur != self.head or flag:
flag = False
if cur.data == data:
return True
cur = cur.next
return False
if __name__ == '__main__':
l1 = SingleCycleLinkList() # 新建一个链表类
l1.add(1)
l1.add(2)
l1.add(3)
l1.add(4)
l1.add(2)
print(l1.nodes_list())
print(l1.search(5))
# l1.insert(7, 7)
# print(l1.nodes_list())
# print(l1.search(11))
3.4双向链表
3.4.1 双向链表的实现
class Node:
# 节点类
def __init__(self, data, _prev=None, _next=None):
self.prev = _prev # 指针域 指向的是当前节点的前一个节点
self.data = data # 数据域
self.next = _next # 指针域 指向的是当前节点的下一个节点
class DoubleLinkList:
def __init__(self):
self.head = None # 链表的头结点
self._length = 0 # 链表的长度,链表的元素个数
def is_empty(self):
# 判断链表是否为空
return self._length == 0
def length(self):
# 返回链表的长度
return self._length
def nodes_list(self):
# 返回链表中的所有节点的值组成的列表
res = []
cur = self.head
while cur:
res.append(cur.data)
cur = cur.next
return res
def add(self, data):
# 往链表的头部添加一个节点,值为data
# 新建一个节点node
node = Node(data)
# None.prev
if self.is_empty():
self.head = node
else:
self.head.prev = node # 让链表中原本的头结点的prev指向新建的节点
# 先让node指向当前链表中的头结点
node.next = self.head
# 再让链表的head指向当前node节点
self.head = node
# 添加节点之后,链表的长度加1
self._length += 1
def append(self, data):
# 往链表的尾部添加一个节点,值为data
# 新建一个节点node,值为data
node = Node(data)
# 找到链表的尾节点
# 从头结点开始,遍历链表中的所有节点
# 每次判断当前节点的next是否为空
# 为空说明当前节点就是尾节点
# 不为空 通过当前节点的next去访问下一个节点,
if self.head:
cur = self.head
while cur.next:
cur = cur.next
# 让当前的尾节点的指针域指向node
node.prev = cur # 让node的prev指针域去指向原本的尾节点
cur.next = node # 让原本的尾节点的next去指向新建的节点
else:
self.head = node
# 链表的长度+1
self._length += 1
def insert(self, pos, data):
# 往链表的指定位置插入一个节点,值为data
if pos <= 0:
self.add(data)
elif pos > self._length:
self.append(data)
else:
# 正常的输入
# 第一步 新建一个节点 node
node = Node(data)
# 第二步
cur = self.head
while pos - 1:
cur = cur.next
pos -= 1
# 到这里之后,cur指向的是索引为pos-1的节点
# 让node的prev指向索引为pos-1的节点
node.prev = cur
# 让索引为pos的节点指向新建的节点
cur.next.prev = node
# 让node的next指向索引为pos的节点
node.next = cur.next
# 让索引为pos-1的节点的next指向cur
cur.next = node
self._length += 1
def remove(self, data):
# 删除链表中第一个值为data的结点
cur = self.head
while cur:
if cur.data == data:
# 如果前驱结点为空,说明我们要删除的节点是第一个节点
if cur == self.head:
self.head = cur.next
else:
cur.prev.next = cur.next
if cur.next:
cur.next.prev = cur.prev
self._length -= 1
return 0
cur = cur.next
return -1
def modify(self, pos, data):
# 修改链表中指定位置节点的值
if 0 <= pos < self._length:
cur = self.head
while pos:
cur = cur.next
pos -= 1
cur.data = data
else:
print('你输入的范围不符合要求')
def search(self, data):
# 查找链表中是否有节点的值为data
cur = self.head
while cur:
if cur.data == data:
return True
cur = cur.next
return False
if __name__ == '__main__':
l1 = DoubleLinkList()
print(l1.nodes_list())
l1.add(1)
print(l1.nodes_list())
l1.add(2)
print(l1.nodes_list())
l1.append(3)
print(l1.nodes_list())
l1.insert(7, 4)
print(l1.nodes_list())
l1.modify(1, 11)
print(l1.nodes_list())
print(l1.search(1))
四、栈和队列
1、栈
1.1 线性表
线性表: 由零个或多个数据元素组成的有限序列 除了第一个节点外,均有唯一的前驱结点 除了最后一个节点外,均有唯一的后继结点
线性表主要由顺序存储结构或者链式存储结构
一般线性表: 可以自由的操作节点,例如顺序表,链表
受限线性表: 对节点的操作受到限制,例如栈和队列
1.2 栈的特点
1.3 栈实现操作
1.3.1 栈-----用顺序表实现
# 栈 -- 用顺序表实现
class Stack:
def __init__(self):
# 把列表的最后一个元素作为栈顶
self.__data = []
def push(self, item):
# 添加一个元素item到栈顶
self.__data.append(item)
def pop(self):
# 要判断栈是否为空
if self.is_empty():
raise ValueError('栈为空')
return self.__data.pop()
def top(self):
# 要判断栈是否为空
if self.is_empty():
raise ValueError('栈为空')
return self.__data[-1]
def is_empty(self):
return self.__data == []
def size(self):
return len(self.__data)
if __name__ == '__main__':
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
stack.push(4)
print(stack.pop())
print(stack.pop())
print(stack.pop())
print(stack.pop())
print(stack.pop())
1.3.2 栈-----用链表实现
# 栈 -- 用链表实现
class Node:
def __init__(self, data, _next=None):
self.data = data # 数据域
self.next = _next # 指针域
class Stack:
def __init__(self):
# 以链表的第一个节点作为栈顶
self.__top = None # 栈顶元素
self._size = 0 # 栈的元素个数
def push(self, item):
# 添加一个元素item到栈顶
# 让self.top指向新的节点
# 让新的节点的next指向原本的栈顶
self.__top = Node(item, self.__top)
self._size += 1
def pop(self):
# 要判断栈是否为空
if self.is_empty():
raise ValueError('栈为空')
value = self.__top.data
self.__top = self.__top.next
self._size -= 1
return value
def top(self):
# 要判断栈是否为空
if self.is_empty():
raise ValueError('栈为空')
return self.__top.data
def is_empty(self):
return self._size == 0
def size(self):
return self._size
if __name__ == '__main__':
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
stack.push(4)
print(stack.pop())
print(stack.pop())
print(stack.pop())
print(stack.pop())
print(stack.pop())
1.4 栈的应用
力扣第20题: 有效的括号
给定一个只包括‘(’, ‘)’, ‘[’, ‘]’, ‘{’, ‘}’的字符串
判断该字符串是否有效 有效字符串需满足:
1. 左括号必须用相同类型的右括号闭合
2. 左括号必须以正确的顺序闭合
空字符串可被认为是有效字符串
例如: “{}”结果为True, “({[]})”结果为True, “(]”结果为False, “([)]”结果为False
from stack_02 import Stack
# 遍历字符串
# 遇到左边括号,就入栈
# 遇到右边括号,栈是否为空
# 为空-> False
# 不为空, 弹出栈顶元素
# 弹出的栈顶元素和遇到的右边括号匹配一下,看是否是相同类型
# 是不同类型,返回False
# 是相同类型,继续往下遍历
# 如果字符串全部匹配完了
# 栈为空 -- > True
# 栈不为空 --> False
def func(string):
if len(string) % 2:
return False
stack = Stack()
dic = {
'(': ')',
'[': ']',
'{': '}'
}
for char in string:
# 判断遇到的是左括号还是右括号
if char in '([{':
stack.push(dic[char])
else:
if stack.is_empty() or stack.pop() != char:
return False
return stack.is_empty()
if __name__ == '__main__':
print(func('('*20+')'*19))
2、队列
2.1 队列的特点
2.2 队列实现操作
2.2.1 队列 -- 顺序表实现
# 队列 -- 顺序表实现
class Queue:
def __init__(self, size):
# 以列表的最后一个元素作为队尾
self.items = [None] * size # 先声明长度为size的数据区
self.head = 0 # 对首的索引
self._length = 0 # 队列的长度
self.size = size # 队列的最大长度
def is_empty(self):
return self._length == 0
def length(self):
return self._length
def push(self, item):
if self.length() == self.size:
raise ValueError('队列已满')
# 先算出要添加的元素的索引
idx = (self.head + self.length()) % self.size
self.items[idx] = item
self._length += 1
def pop(self):
# 抛出队首元素
if self.is_empty():
raise ValueError('队列是空的')
value = self.items[self.head]
self.head = (self.head + 1) % self.size
self._length -= 1
return value
def peek(self):
if self.is_empty():
raise ValueError('队列是空的')
return self.items[self.head]
if __name__ == '__main__':
queue = Queue(3)
queue.push(1)
queue.push(2)
queue.push(3)
print(queue.pop()) # 1
queue.push(4)
print(queue.length()) # 3
print(queue.peek()) # 2
print(queue.pop()) # 2
print(queue.pop()) # 3
print(queue.pop()) # 4
print(queue.items)
# print(queue.pop())
2.2.2 队列 -- 链表实现
# 队列 -- 链表实现
class Node:
def __init__(self, data, _next=None):
self.data = data # 数据域
self.next = _next # 指针域
class Queue:
def __init__(self, size):
self.head = None # 队列的队头
self.rear = None # 队列的队尾
self._length = 0 # 队列的长度
self.size = size # 队列的最大长度
def is_empty(self):
return self._length == 0
def length(self):
return self._length
def push(self, item):
# 判断队列是否已满
if self.length() == self.size:
raise ValueError('队列已满')
# 添加一个元素item到队尾
# 最后一个元素作为队尾
node = Node(item)
# 如果队列是空
if self.is_empty():
self.head = node
self.rear = node
else:
self.rear.next = node
self.rear = node
self._length += 1
def pop(self):
# 抛出队首元素
if self.is_empty():
raise ValueError('队列是空的')
value = self.head.data
self.head = self.head.next
self._length -= 1
return value
def peek(self):
if self.is_empty():
raise ValueError('队列是空的')
return self.head.data
if __name__ == '__main__':
queue = Queue(3)
queue.push(1)
queue.push(2)
queue.push(3)
print(queue.pop()) # 1
queue.push(4)
print(queue.length()) # 3
print(queue.peek()) # 2
print(queue.pop()) # 2
print(queue.pop()) # 3
print(queue.pop()) # 4
# print(queue.pop())
2.3 思考题
在现实生活中,我们的队列常常会有长度限制,比如队列中已经有n个元素了,那么就提示队列已满
你能否实现一个限制长度的队列呢?
如果实现一个限制长度的队列, 用顺序结构该如何去实现才能做到所有操作的时间复杂度都是O(1)呢?
3、 双端队列
3.1 双端队列特点
3.2 双端队列实现操作
3.2.1 双端队列 - 顺序表实现
# 双端队列 - 顺序表实现
class Deque:
def __init__(self):
self.items = []
def is_empty(self):
return self.items == []
def length(self):
return len(self.items)
def push(self, item):
self.items.append(item)
def push_left(self, item):
self.items.insert(0, item)
def pop(self):
return self.items.pop(0)
def pop_right(self):
return self.items.pop()
def peek(self):
return self.items[0]
if __name__ == '__main__':
deque = Deque()
deque.push(1) # 1
deque.push(2) # 1, 2
deque.push_left(3) # 3, 1, 2
deque.push_left(4) # 4, 3, 1, 2
print(deque.items)
deque.pop()
print(deque.items) # 3, 1, 2
deque.pop_right()
print(deque.items) # 3, 1
3.2.2 双端队列 - 链表实现
# 双端队列 -- 链表实现
class Node:
def __init__(self, data, _next=None):
self.data = data # 数据域
self.next = _next # 指针域
class Deque:
def __init__(self):
self.head = None # 队首
self.rear = None # 队尾
self._length = 0 # 队列的长度
def is_empty(self):
return self._length == 0
def length(self):
return self._length
def items(self):
cur = self.head
while cur:
print(cur.data, '->', end=' ')
cur = cur.next
print()
def push(self, item):
# 在队尾添加一个元素item
node = Node(item)
# 队列为空
if self.is_empty():
self.head = node
self.rear = node
# 队列不为空
else:
self.rear.next = node
self.rear = node
self._length += 1
def push_left(self, item):
# 在队首添加一个元素item
node = Node(item)
if self.is_empty():
self.head = node
self.rear = node
else:
node.next = self.head
self.head = node
self._length += 1
def pop(self):
# 弹出队首元素
if self.is_empty():
raise ValueError('双端队列为空')
value = self.head.data
self.head = self.head.next
self._length -= 1
if self._length == 0:
self.rear = None
return value
def pop_right(self):
# 弹出队尾元素
if self.is_empty():
raise ValueError('双端队列为空')
if self.length() == 1:
return self.pop()
cur = self.head
value = self.rear.data
while cur.next != self.rear:
cur = cur.next
self.rear = cur
cur.next = None
self._length -= 1
return value
def peek(self):
if self.is_empty():
raise ValueError('双端队列为空')
return self.head.data
if __name__ == '__main__':
deque = Deque()
deque.push(1) # 1
deque.push(2) # 1, 2
deque.push_left(3) # 3, 1, 2
deque.push_left(4) # 4, 3, 1, 2
deque.items()
deque.pop()
deque.items()
deque.pop_right()
deque.items()
五、排序
1、排序算法的稳定性
稳定的排序算法会让原本有相等键值的记录维持相对次序;
不稳定的排序算法可能会改变相等键值的记录的相对次序。
2、冒泡排序
- 重复比较相邻的元素,如果前面的比后面的大,就交换它们两个
- 每次遍历整个数组,遍历完成后,下一次遍历的范围往左缩1位
- 重复前面步骤,直到排序完成
- 时间复杂度o(n^2)
# 冒泡排序(稳定的排序) def bubbleSort(nums): n = len(nums) # 得到数组的长度 for i in range(n-1): flag = False # 表示本轮是否有进行变量交换 for idx in range(0, n-1-i): if nums[idx] > nums[idx+1]: nums[idx], nums[idx+1] = nums[idx+1], nums[idx] flag = True print(f'第{i+1}趟排序:', nums) # 如果flag为False,那说明本轮排序没有进行任何变量交换 # 数组已经是有序的了, if not flag: break # O(n^2) test = [6, 5, 4, 3, 2, 1] bubbleSort(test) # 1, 2, 3, 4, 5, 6 print(test)
3、选择排序
- 初始状态: 有序区为空, 无序区为[0,…,n]
- 每次找到无序区里的最小元素,添加到有序区的最后
- 重复前面步骤,直到排序完成
- 时间复杂度o(n^2)
# 选择排序(是不稳定的排序) def selectionSort(nums): n = len(nums) # 数组的长度 print(nums) for i in range(n-1): # 找无序区中最小的元素 min_idx = i # 无序区中的最小元素的索引 for j in range(i+1, n): if nums[j] < nums[min_idx]: min_idx = j # 执行完上面的循环后 # min_idx就是无序区中的最小元素的索引 # 把最小元素和有序区的后一个元素交换位置 nums[i], nums[min_idx] = nums[min_idx], nums[i] print(f'第{i+1}排序:', nums) test = [9, 3, 1, 2, 4, 7] selectionSort(test) print(test)
4、插入排序
每次都将后面没排好序的数插入到前面排好序的数中。(稳定的排序)
时间复杂度o(n^2)
# 插入排序 def insertSort(nums): n = len(nums) # 数组的长度 # 设定一个增量gap # gap = n // 2 # while gap >= 1: # 分组 + 对每一组进行插入排序 # 缩小增量 gap //= 2 for i in range(n-1): curNum = nums[i+1] # 无序区的第一个元素的值 idx = i # 有序区的最后一个元素的索引 while idx >= 0 and nums[idx] > curNum: nums[idx+1] = nums[idx] # 把有序区的元素往后挪一位 idx -= 1 # 指针往前移,以此来从后往前遍历有序区 nums[idx+1] = curNum print(f'第{i+1}趟排序', nums) test = [9, 3, 1, 2, 7, 5] insertSort(test) print(test)
5、希尔排序
希尔排序是插入排序的优化版本,步骤:
- 设定一个增量gap,将数组按照gap分组
- 对每一组进行插入排序
- 缩小增量gap,重复前两个步骤,直到gap缩小到1,那么最后一次排序就是 插入排序
# 希尔排序 def shellSort_1(nums): n = len(nums) # 数组的长度 # 设定一个增量gap gap = 1 while gap < n // 3: gap = gap * 3 + 1 while gap >= 1: # 分组 for i in range(gap): # gap=5, i=>0,1,2,3,4 # 对每一个小组进行插入排序 for j in range(i, n-gap, gap): curNum = nums[j+gap] # 无序区的第一个元素的值 idx = j # 有序区的最后一个元素的索引 while idx >= 0 and nums[idx] > curNum: nums[idx+gap] = nums[idx] # 把小组的有序区的元素往后挪 idx -= gap # 指针往前移,以此来从后往前遍历小组的有序区 nums[idx+gap] = curNum print(f'当前增量是{gap}, ', nums) gap //= 3 # 缩小增量 def shellSort_2(nums): n = len(nums) # 数组的长度 # 设定一个增量gap gap = n // 2 while gap >= 1: # 分组 for i in range(gap, n): # i = 5, curNum=43, idx = 0 # [43, ..., 44, ...., 85] # i=7, curNum=7, idx=2 # [..., 7, ... 59, ...] curNum = nums[i] # 当前要插入的无序区的元素的值 idx = i - gap # 当前元素所在小组的有序区的最后一个元素的索引 while idx >= 0 and curNum < nums[idx]: nums[idx+gap] = nums[idx] idx -= gap nums[idx+gap] = curNum gap //= 2 # 缩小增量 # gap=5 [44, 43, 85], [12, 94], [59, 7], [36, 35], [62, 52] test = [44, 12, 59, 36, 62, 43, 94, 7, 35, 52, 85] shellSort_2(test) print(test)
6、快速排序(重点)
分而治之
- 设定一个基准值pivot
- 将数组重新排列,所有比pivot小的放在其前面,比pivot大的放后面,这操作 称为分区(partition)操作
- 对两边的数组重复前两个步骤
# 快速排序 def quickSort(nums): n = len(nums) if n<=1: return nums pivot = nums[0] left = [] right = [] for i in range(1,n): if nums[i]>pivot: right.append(nums[i]) else: left.append(nums[i]) print(left) print(right) print('-'*30) return quickSort(left)+[pivot]+quickSort(right)
# 快速排序的优化 """ 1.基准值的选取 (1)随机选取 (2)三数 mid 2.排序序列长度到一定大小后,改用插入排序 3.重复元素的处理 每次分割时,将与本次基准值相等的元素聚集在一起 (1)遇到相等的元素,放到区域的最左边或最右边 (2)分好区之后,相等的元素与基准值一边的元素进行交换 4.尾递归 """def partition(nums, left, right): pivot = nums[left] # 区域的第一个元素作为基准值 while left < right: # 挖坑,填坑 while left < right and nums[right] > pivot: right -= 1 nums[left] = nums[right] while left < right and nums[left] <= pivot: left += 1 nums[right] = nums[left] nums[left] = pivot # 基准值的正确位置 return left def quickSort(nums, left, right): if left >= right: return # 分区 --> 分好区之后的基准值的索引 pivot_idx = partition(nums, left, right) print(nums, pivot_idx) # 左边的区域, left->pivot_idx-1 quickSort(nums, left, pivot_idx-1) # 右边的区域, pivot_idx+1->right quickSort(nums, pivot_idx+1, right) test = [44, 12, 59, 36, 62, 43, 94, 7, 35, 52, 85] quickSort(test, 0, len(test)-1) print(test)
7、归并排序
# 归并排序
def merge(left, right):
# 最终返回一个合并好的有序的数组
# 定义两个变量,分别代表当前left与right的未添加进有序数组的第一个元素
left_idx, right_idx = 0, 0
res = [] # 有序数组
while left_idx < len(left) and right_idx < len(right):
# 左边数组的元素小于右边数组
if left[left_idx] <= right[right_idx]:
# 把左边元素添加到有序区中
res.append(left[left_idx])
# 索引往后移
left_idx += 1
else:
# 把右边元素添加到有序区中
res.append(right[right_idx])
# 索引往后移
right_idx += 1
res += right[right_idx:] # 把剩余的未添加的元素全部添加到有序数组后面
res += left[left_idx:] # 为什么可以直接添加?因为left,right本身就是一个有序数组
# 如果说left_idx走完了,right还剩一些元素,说明right剩下的元素全部都比有序数组的最后一个元素要大
return res
def mergeSort(nums):
# 分
# 数组不能再分了
if len(nums) <= 1:
return nums
mid = len(nums) // 2 # 求出数组的中间位置
print(nums[:mid], nums[mid:])
left = mergeSort(nums[:mid]) # 左边的数组
right = mergeSort(nums[mid:]) # 右边的数组
# 合
return merge(left, right)
test = [44, 12, 59, 36, 62, 43, 94, 7, 35, 52, 85]
test = mergeSort(test)
print(test)
# 时间复杂度 O(nlogn)
# 稳定性 稳定
8、桶排序
# 桶排序
def bucketSort(nums, size=5):
# 根据数组的最大值与最小值确定要申请的桶的数量
maxVal = max(nums) # 最大值
minVal = min(nums) # 最小值
bucketCount = (maxVal - minVal) // size + 1 # 桶的数量
buckets = [[] for _ in range(bucketCount)] # 申请桶
for num in nums:
idx = (num - minVal) // size # num应该在哪个桶中,索引为idx
n = len(buckets[idx]) # 求出当前桶中的元素个数
i = 0
# 找到第一个比num要大的元素
while i < n and buckets[idx][i] <= num:
i += 1
buckets[idx].insert(i, num)
print(buckets)
# 合并桶
nums.clear()
for bucket in buckets:
nums.extend(bucket) # 将每个桶中的元素放到nums中
test = [1, 2, 3, 3, 3, 3, 3, 99]
bucketSort(test)
print(test)
9、哈希排序
空间换时间的一种算法,时间复杂度o(1)
9.1 哈希冲突
9.1.1 开链法
适合数据量比较大的
9.1.2 多哈希法
有多个哈希函数,当使用一个哈希函数发生冲突时,尝试下一个哈希函数,直到冲突不再发生。
9.1.3 开放寻址法
适合数据量比较大的
如果哈希函数得到的位置i已经有数据了,那么就往后探查新的位置来存储这个值
线性探测 如果i有数据了,则探查i+1,i+2..以此类推,直到找到空的位置
二次探测 如果位置i被占用,则探查i+1^2,i+2^2…以此类推,直到找到空的位置
9.2 扩容问题
使用开放寻址法,那么顺序表总归会有一天会填满
一般为了保证插入和查找的效率,哈希表一般在元素数量在容量的2/3时
就会进行扩容 扩容之后,计算的哈希函数也会随之变化,那么里面的数据存储的顺序也会 变化
10、二分查找(重点)
10.1 查找
在一组数据中找某一个特定项的算法过程
通常用来判断某个特定项是否在一组数据中,最终返回True或False
常用的查找算法: 顺序查找,二分查找,树表查找,哈希查找等
顺序查找:遍历列表,看数据是否等于目标查找的数
10.2 二分查找
又称折半查找
要求待查表为有序表
将表中间位置记录的关键字与查找关键字比较,如果相等则比较成功;否则 利用中间位置的记录缩小区间,继续查找缩小后的区间.
重复上面的步骤直到查找成功,或者子表不存在,则查找失败.
# 二分查找
# 递归
def binary_search(nums, target, left, right):
"""
二分查找递归版
:param nums: 待查找的数组,要求是升序的
:param target: 要找的数字
:param left: 区间的左边索引
:param right: 区间的右边索引
:return: target在nums中就返回True,否则返回False
"""
# 递归的结束条件, left > right
print(nums[left:right+1])
if left > right:
return False
# 找中间值
mid = (left + right) // 2 # 中间值的索引
print(mid)
# 判断中间值是否等于目标值
if nums[mid] == target:
return True
# 如果中间值小于目标值,说明目标值只可能在中间值的右边区间
if nums[mid] < target:
# 左边区间的范围往右边缩
return binary_search(nums, target, mid + 1, right)
# 如果中间值大于目标值,说明目标值只可能在中间值的左边区间
return binary_search(nums, target, left, mid - 1)
test = [1, 3, 4, 6, 8, 9, 15, 19, 44, 44]
print(binary_search(test, 15, 0, len(test)-1)) # True
print(binary_search(test, 14, 0, len(test)-1)) # False
六、树
1、树&二叉树
1.1 树
由n个有限节点组成一个具有层次关系的集合,看起来像一颗 倒挂的树。
特点:
- 每个节点有0个或多个子节点
- 没有父节点的节点称为根节点
- 每一个非根节点有且只有一个父节点
- 除了根节点外,每个子节点可以分为多个不相交的子树
1.2 树的语术
节点的度: 一个节点含有的子树的个数
树的度: 树中所有节点的度的最大值
叶节点: 度为0的节点
子节点: 一个节点含有的子树的根节点称为该节点的子节点
父节点: 若一个节点有子节点,那么这个节点就是其子节点的父节点
兄弟节点: 具有相同父节点的节点互称兄弟节点
堂兄弟节点: 在同一层的节点互称堂兄弟节点
祖先节点: 从根到该节点所经路径上的所有节点
子孙节点: 以某节点为根的子树中的所有节点
节点层次: 根节点层次为1,其他节点层次是父节点的层次加1
树的深度: 树中所有节点的层次的最大值
森林: 多颗不相交的树的集合
1.3 二叉树
二叉树: 每个节点最多含有两个子树的树称为二叉树
完全二叉树: 除了最底层外,其他各层的节点数目均达到最大值,且最底层的节点应从左往右紧密排列
满二叉树: 所有叶节点都在最底层的完全二叉树
二叉搜索树: 对于一个节点,它的左子树上的所有节点的值都比它小,右子树上的所有节点的值都比它大。
1.4 二叉树存储结构
顺序存储: 从上往下,从左往右的将树存到顺序表中
优点: 遍历方便,可以用索引来表示节点间的关系
缺点: 可能会对存储空间造成极大的浪费 适用于存完全二叉树
链式存储: 每个节点具有 左指针域, 数据域, 右指针域, 以此来连接.
2、树的广度优先遍历
class Node:
def __init__(self, val):
self.val = val # 数据域
self.left = None # 左指针域
self.right = None # 右指针域
class Tree:
def __init__(self):
self.root = None # 树的根节点
def add(self, val):
# 层次遍历, 广度优先遍历
# val 要添加的节点的值
# 往树中添加一个节点,并保证添加之后这棵树依旧是一棵完全二叉树
node = Node(val)
# 判断树是否为空,如果为空,直接将node设置为根节点
if not self.root:
self.root = node
return
# 从上往下,从左往右的去遍历整棵树,然后找到第一个空位
# 把节点添加进去
queue = [self.root] # 存每一层的节点
while True:
# 第一次 queue = [root]
# 第二次 queue = [root.left, root.right]
# queue = [root.right, root.left.left, root.left.right]
# queue = [root.left.left, root.left.right, root.right.left, root.right.right]
cur_node = queue.pop(0)
# 先找左边,看有没有空位
if not cur_node.left:
cur_node.left = node
return
# 左边没有空位,就找右边
elif not cur_node.right:
cur_node.right = node
return
# 如果都没有空位,那就把左边节点与右边节点都加到之后要判断的节点中
queue.extend((cur_node.left, cur_node.right))
def show(self):
# 展示树
if not self.root:
return
# 从上往下,从左往右的去遍历整棵树,然后找到第一个空位
# 把节点添加进去
queue = [self.root] # 存每一层的节点
i = 1
while queue:
size = len(queue) # 当前层的元素个数
print(f'第{i}层', end='\t')
for _ in range(size):
node = queue.pop(0) # 队列中的第一个元素抛出来
print(node.val, end=' ') # 对当前元素进行操作
# 节点的左孩子与右孩子添加到队列里
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
print()
i += 1
tree=Tree()
tree.add(0)
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
tree.add(6)
tree.add(7)
tree.add(8)
tree.show()
3、树的深度优先遍历
先序遍历:
中序遍历:
后序遍历:
class Node:
def __init__(self, val):
self.val = val # 数据域
self.left = None # 左指针域
self.right = None # 右指针域
class Tree:
def __init__(self):
self.root = None # 树的根节点
def add(self, val):
# 层次遍历, 广度优先遍历
# val 要添加的节点的值
# 往树中添加一个节点,并保证添加之后这棵树依旧是一棵完全二叉树
node = Node(val)
# 判断树是否为空,如果为空,直接将node设置为根节点
if not self.root:
self.root = node
return
# 从上往下,从左往右的去遍历整棵树,然后找到第一个空位
# 把节点添加进去
queue = [self.root] # 存每一层的节点
while True:
# 第一次 queue = [root]
# 第二次 queue = [root.left, root.right]
# queue = [root.right, root.left.left, root.left.right]
# queue = [root.left.left, root.left.right, root.right.left, root.right.right]
cur_node = queue.pop(0)
# 先找左边,看有没有空位
if not cur_node.left:
cur_node.left = node
return
# 左边没有空位,就找右边
elif not cur_node.right:
cur_node.right = node
return
# 如果都没有空位,那就把左边节点与右边节点都加到之后要判断的节点中
queue.extend((cur_node.left, cur_node.right))
def show(self):
# 展示树
if not self.root:
return
# 从上往下,从左往右的去遍历整棵树,然后找到第一个空位
# 把节点添加进去
queue = [self.root] # 存每一层的节点
i = 1
while queue:
size = len(queue) # 当前层的元素个数
print(f'第{i}层', end='\t')
for _ in range(size):
node = queue.pop(0) # 队列中的第一个元素抛出来
print(node.val, end=' ') # 对当前元素进行操作
# 节点的左孩子与右孩子添加到队列里
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
print()
i += 1
def preorder(self):
# 先序遍历 前序遍历 根 左子树 右子树
def helper(root):
if not root:
return
print(root.val, end=' ') # 输出根节点
helper(root.left) # 先序遍历左子树
helper(root.right) # 先序遍历右子树
helper(self.root)
print()
def inorder(self):
# 中序遍历 左子树 根 右子树
def helper(root):
if not root:
return
helper(root.left) # 中序遍历左子树
print(root.val, end=' ') # 输出根节点
helper(root.right) # 中序遍历右子树
helper(self.root)
print()
def postorder(self):
# 后序遍历 左子树 右子树 根
def helper(root):
if not root:
return
helper(root.left) # 后序遍历左子树
helper(root.right) # 后序遍历右子树
print(root.val, end=' ') # 输出根节点
helper(self.root)
print()
4、根据前序遍历与中序遍历确定树
问题:如何通过一棵无重复节点的树的
先序遍历0->1->3->7->8->4->9->2->5->6
与 中序遍历7->3->8->1->9->4->0->5->2->6
去构造这棵树的结构?
def buildTree(preorder, inorder): """ 根据前序遍历与中序遍历去构建一棵无重复节点的二叉树 :param preorder: 前序遍历的结果, list :param inorder: 中序遍历的结果, list :return: 树的根节点 """ # 递归结束条件 if not preorder: return None in_idx = inorder.index(preorder[0]) # 在中序遍历结果中找到根节点的索引 root = Node(preorder[0]) # 构建根节点 root.left = buildTree(preorder[1:in_idx+1], inorder[:in_idx]) # 构建左子树 root.right = buildTree(preorder[in_idx+1:], inorder[in_idx+1:]) # 构建右子树 return root if __name__ == '__main__': tree = Tree() tree.root = buildTree( [0, 1, 3, 7, 8, 4, 9, 2, 5, 6], [7, 3, 8, 1, 9, 4, 0, 5, 2, 6] ) tree.show()