本章所需相关基础知识:
- Python基础学习笔记(二)—— 数据类型及操作
- Python基础学习笔记(六)—— 面向对象编程(1)
self 可以看成当前类的一个对象,可以调用这个类里的函数和变量
1. 数组的基本结构
数组是最常见的一种数据结构,其由有限个类型相同的变量按照一定的顺序组合构成,在Python中常常利用列表(list)来表示数组。Python定义数组的时候与C/C++中定义数组时的区别在于定义时无序指定长度,可以动态增长,不断向后追加元素,一般不会出现数组溢出的状况,为编程者带来了极大的自由度。
# 一维数组
arr1 = [1, 2, 3, 4]
print(arr1) # [1, 2, 3, 4]
print(arr1[0]) # 1
# 二维数组
arr2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
print(arr2) # [[1, 2, 3, 4], [5, 6, 7, 8]]
print(arr2[0][3]) # 4
# 一般通过如下方式定义一个二维数组
arr3 = [[i for i in range(4)] for j in range(3)]
print(arr3) # [[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
2. 数组的基本操作
# 定义
arr = [1, 2, 3, 4, 5, 1]
# 增加
# arr.append(6)
# print(arr) # [1, 2, 3, 4, 5, 1, 6]
# 删除(利用pop(),remove(),del()方法删除元素)
# 1. pop([索引]) 删除指定索引对应的元素,默认删除数组最后一个元素,并返回该值
# arr.pop()
# arr.pop(1)
# 2. remove(元素值) 删除数组里某个值的第一个匹配项
# arr.remove(1)
# 3. del() 按照索引删除元素
# del(arr[2])
# del arr[2]
# 插入
# insert(插入的索引位置,插入的元素)
# arr.insert(0, 100)
# 查找
# 1. 想确定数组中是否含有某一个元素
# if 200 in arr:
# print("True")
# 2. 想确定某个元素的索引,index(元素值) 查找数组中该元素第一次出现的索引
# arr.index(1)
# 修改
# 通过索引直接访问重新赋值即可
# arr[0] = 9
# 反转
# reverse()方法反转列表,直接对数组进行操作,没有产生额外的空间
# arr.reverse()
# 排序
# sort(key=None,reverse=False)默认升序,修改reverse=True则为降序
# arr.sort()
# arr.sort(reverse=True)
# 清空
# 对数组进行清空,输出[]
# arr.clear()
# 截取
# 按步长截取,顾头不顾尾
# 数组名[起始索引(不写则默认包含数组开始的所有元素),终止索引(不写则默认包含到数组结束的所有元素),步长(默认为1)]
# print(arr[::2]) # [1, 3, 5]
# print(arr[::-1]) # [1, 5, 4, 3, 2, 1]
# print(arr[:-1]) # [1, 2, 3, 4, 5]
1. 链表的基本结构
链表主要包括单向链表和双向链表,这是一种无须在内存中顺序存储即可保持数据之间逻辑关系的数据结构。
链表是由一个个结点(Node)连接而成的,每个结点都是包含数据域(Data)和指针域(Next)的基本单元。其基本元素如下:
单链表
单链表的每个结点的指针域只指向下一个结点,整个链表是无环的
双向链表
单向循环链表
相比于数组,在链表中执行插入、删除等操作可以使得操作效率大大提高。
以单链表为例,在数组内如想要删除或者插入元素到某一位置,该位置之后的所有元素都需要象前或者向后移动,这样一来,时间复杂度就与数组的长度有关,为O(n);但是在单链表中,仅仅需要通过改变所要删除或者插入位置前后结点的指针域即可,时间复杂度为O(1)。
2. 单链表的实现与基本操作
# 链表结点
class Node(object):
def __init__(self, item):
self.item = item
self.next = None
# 单链表
class SingleLink(object):
# 选择该初始化方法,调用时使用 SingleLink(node)
def __init__(self, node=None):
self.head = node
# 选择该初始化方法,调用时就不能使用上面的 SingleLink(node),初始化只能通过append添加结点
# def __init__(self):
# self.head = None
# 判断单链表是否为空
def is_empty(self):
if self.head is None:
return True
else:
return False
# 获取链表长度
def length(self):
cur = self.head
count = 0
while cur is not None:
cur = cur.next
count += 1
return count
# 遍历链表
def travel(self):
cur = self.head
while cur is not None:
print(cur.item, end=" ")
cur = cur.next
# 链表头部增加结点
def add(self, item):
node = Node(item)
node.next = self.head
self.head = node
# 链表尾部增加结点
"""
注意:如果链表为空链表,cur是没有next的,只需self.head=node
"""
def append(self, item):
node = Node(item)
if self.is_empty():
self.head = node
else:
cur = self.head
while cur.next is not None:
cur = cur.next
cur.next = node
# 链表指定位置增加结点
"""
注意:这个只适用于链表中不存在重复元素的,要区别LeetCode203. 移除链表元素(这个题链表里会存在重复元素)
"""
def insert(self, pos, item):
if pos == 0:
self.add(item)
elif pos >= self.length():
self.append(item)
else:
node = Node(item)
cur = self.head
count = 0
while count < pos - 1:
cur = cur.next
count += 1
node.next = cur.next
cur.next = node
# 删除结点
def remove(self, item):
cur = self.head
pre = None
while cur is not None:
# 找到了要删除的元素
if cur.item == item:
# 如果要删除的位置在头部
if cur == self.head:
self.head = cur.next
# 要删除的位置不在头部
else:
pre.next = cur.next
return # 删除元素后及时退出循环
# 没有找到要删除的元素
else:
pre = cur
cur = cur.next
# 查找结点
def search(self, item):
cur = self.head
while cur is not None:
if cur.item == item:
return True
cur = cur.next
return False
1. 队列的基本结构
队列最基本的特点就是先进先出,在队列尾部加入新元素,在队列头部删除元素,分为双端队列和一般的单端队列。
队列的作用:对于任务处理类的系统,即先把用户发起的任务请求接收过来存到队列中,然后后端开启多个应用程序从队列中取任务进行处理,队列起到了 缓冲压力 的作用
2. 队列的实现与基本操作
利用列表来简单地模拟队列
class Queue(object):
def __init__(self):
self.items = []
# 入队
def enqueue(self, item):
self.items.append(item)
# 出队
def dequeue(self):
self.items.pop(0)
# 队列的大小
def size(self):
return len(self.items)
# 判断队列是否为空
def is_empty(self):
return self.items == []
对于队列这种数据结构,Python的 queue 类模块中提供了一种先进先出的队列模型 Queue,可以限制队列的长度也可以不限制,在创建队列时利用 Queue(maxsize=0)
,maxsize小于等于0表示不限制,否则表示限制。
我们在编程的过程中也可以通过调用现有类来实现队列
from queue import Queue
# 队列的定义
q = Queue(maxsize=0)
# put() 在队列尾部添加元素
q.put(1)
q.put(2)
# print(q) #
# print(q.queue) # deque([1, 2])
# get() 在队列头部取出元素,返回队列头部元素
q.get()
print(q.queue) # deque([2])
# empty() 判断队列是否为空
print(q.empty()) # False
# full(0 判断队列是否达到最大长度限制
print(q.full()) # False
# qsize() 队列当前的长度
print(q.qsize()) # 1
3. 双端队列的实现与基本操作
双端队列(deque,全名double-ended queue ), 是一种具有队列和栈的性质的数据结构
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
class deque(object):
def __init__(self):
self.items = []
# 判断是否为空
def is_empty(self):
return self.items == []
# 队列的大小
def size(self):
return len(self.items)
# 头部添加数据
def add_front(self, item):
self.items.insert(0, item)
# 尾部添加数据
def add_rear(self, item):
self.items.append(item)
# 头部删除数据
def remove_front(self):
self.items.pop(0)
# 尾部删除数据
def remove(self):
self.items.pop()
1. 栈的基本结构
栈最突出的特点是先进后出,其插入、删除操作均在栈顶进行。栈一般包括入栈、出栈操作,并且有一个顶指针(top)用于指示栈顶的位置
2. 栈的实现与基本操作
class Stack(object):
def __init__(self):
self.items = []
# 进栈
def push(self, item):
self.items.append(item)
# 出栈
def pop(self):
self.items.pop()
# 遍历
def travel(self):
for i in self.items:
print(i)
# 栈的大小
def size(self):
return len(self.items)
# 栈是否为空
def is_empty(self):
return self.items == []
# return len(self.items) == 0
# 返回栈顶元素
def peek(self):
if self.is_empty():
return "栈空"
return self.items[self.size()-1]
# return self.items[-1]
1. 树
树是一种数据结构,它是由 n 个有限结点组成的一个具有层次关系的集合。
树的基本性质如下:
2. 二叉树的基本结构
二叉树则是每个结点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。
二叉树的一般性质:
其他常见的二叉树:
注意:
- 如果按层序从0开始编号,结点 i 的左孩子为:2i+1,结点 i 的右孩子为:2i+2,结点 i 的父结点为:(i-1)//2
- 如果结点按层序从0开始编号,假设共有 n 个结点,若 i <= n//2-1 ,该结点为非终端结点,若 i > n//2-1 ,该结点为终端结点
二叉树通常以链式存储
3. 二叉树的实现与基本操作
# 定义结点类
class Node(object):
def __init__(self, item):
self.item = item
self.lchild = None
self.rchild = None
# 定义二叉树
class BinaryTree(object):
def __init__(self, node=None):
self.root = node
"""
思路分析:首先在队列中插入根结点,取出该结点,再判断该结点的左右子树是否为空,
左子结点不空,将其入队,右子结点不空,将其入队,
再分别判断左右结点的左右子结点是否为空,
循环往复,直到发现某个子结点为空,即把新结点添加进来
"""
# 添加结点
def add(self, item):
node = Node(item)
# 二叉树为空
if self.root is None:
self.root = node
return
# 二叉树不空
queue = []
queue.append(self.root)
# 编译环境会提示,也可以直接写成:queue = [self.root]
while True:
# 从队头取出数据
node1 = queue.pop(0)
# 判断左结点是否为空
if node1.lchild is None:
node1.lchild = node
return
else:
queue.append(node1.lchild)
# 判断右结点是否为空
if node1.rchild is None:
node1.rchild = node
return
else:
queue.append(node1.rchild)
# 广度优先遍历,也叫层次遍历
def breadth(self):
if self.root is None:
return
queue = []
queue.append(self.root)
while len(queue) > 0:
# 取出数据
node = queue.pop(0)
print(node.item, end=" ")
# 判断左右子结点是否为空
if node.lchild is not None:
queue.append(node.lchild)
if node.rchild is not None:
queue.append(node.rchild)
# 深度优先遍历
# 先序遍历(根左右)
def preorder_travel(self, root):
if root is not None:
print(root.item, end=" ")
self.preorder_travel(root.lchild)
self.preorder_travel(root.rchild)
# 中序遍历(左根右)
def inorder_travel(self, root):
if root is not None:
self.inorder_travel(root.lchild)
print(root.item, end=" ")
self.inorder_travel(root.rchild)
# 后序遍历(左右根)
def postorder_travel(self, root):
if root is not None:
self.postorder_travel(root.lchild)
self.postorder_travel(root.rchild)
print(root.item, end=" ")
if __name__ == "__main__":
tree = BinaryTree()
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
# 添加结点的代码逻辑就是将添加第一个结点设置为根结点
print(tree.root)
print()
# 层序遍历
tree.breadth() # 1 2 3 4
print()
# 前序遍历(根左右)
tree.preorder_travel(tree.root) # 1 2 4 3
print()
# # 中序遍历(左根右)
tree.inorder_travel(tree.root) # 4 2 1 3
print()
# # 后序遍历(左右根)
tree.postorder_travel(tree.root) # 4 2 3 1
注意:
- 广度优先遍历基于队列
- 深度优先遍历基于栈
试试 LeetCode 相关题目吧
注意:二叉树遍历相关的题目在LeetCode环境中,省略了添加结点的代码逻辑,以及定义二叉树类中的初始化,要注意区分、根据具体题目应变。另外,其输入输出都是列表的形式,要注意
4. 由遍历结果反推二叉树结构
算法的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变则称这种排序算法是稳定的,否则称为不稳定的
对要进行排序的数据中相邻的数据进行两两比较,将较大的数据放在后面,依次对所有的数据进行操作,直至所有数据按要求完成排序
如果有n个数据进行排序,总共需要比较 n-1 次
每一次比较完毕,下一次的比较就会少一个数据参与
def bubble_sort(lis):
n = len(lis)
# 控制比较的轮数
for j in range(n - 1):
count = 0
# 控制每一轮的比较次数
# -1是为了让数组不要越界
# -j是每一轮结束之后, 我们就会少比一个数字
for i in range(n - 1 - j):
if lis[i] > lis[i + 1]:
lis[i], lis[i + 1] = lis[i + 1], lis[i]
count += 1
# 算法优化
# 如果遍历一遍发现没有数字交换,退出循环,说明数列是有序的
if count == 0:
break
if __name__ == "__main__":
lis = [2, 7, 3, 6, 9, 4]
bubble_sort(lis)
print(lis)
总结:
- 冒泡排序是稳定的
- 最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)
- 最优时间复杂度为 O ( n ) O(n) O(n),遍历一遍发现没有任何元素发生了位置交换终止排序
快速排序算法中,每一次递归时以第一个数为基准数 ,找到数组中所有比基准数小的。再找到所有比基准数大的。小的全部放左边,大的全部放右边,确定基准数的正确位置。
def quick_sort(lis, left, right):
# 递归的结束条件:left > right
if left > right:
return
# 存储临时变量,left0始终为0,right0始终为len(lis)-1
left0 = left
right0 = right
# 基准值
base = lis[left0]
# left != right
while left != right:
# 从右边开始找寻小于base的值
while lis[right] >= base and left < right:
right -= 1
# 从左边开始找寻大于base的值
while lis[left] <= base and left < right:
left += 1
# 交换两个数的值
lis[left], lis[right] = lis[right], lis[left]
# left=right
# 基准数归位
lis[left0], lis[left] = lis[left], lis[left0]
# 递归操作
quick_sort(lis, left0, left - 1)
quick_sort(lis, left + 1, right0) # quick_sort(lis, left + 1, right0)
if __name__ == '__main__':
lis = [1, 2, 100, 50, 1000, 0, 10, 1]
quick_sort(lis, 0, len(lis) - 1)
print(lis)
总结:
- 快速排序算法不稳定
- 最好的时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),初始序列大小均匀,每一次选择的基准值将待排序的序列划分为均匀的两部分,递归深度最小,算法效率最高
- 最坏的时间复杂度: O ( n 2 ) O(n^2) O(n2),初始序列有序或逆序,每次选择的基准值都是靠边的元素,递归深度最大,算法效率最低
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾,以此类推,直到全部待排序的数据元素的个数为零。
def select_sort(lis):
n = len(lis)
# 控制比较的轮数
for j in range(n - 1):
# 假定最小值的下标
min_index = j
# 控制每一轮的比较次数
for i in range(j + 1, n):
# 进行比较获得最小值下标
if lis[min_index] > lis[i]:
min_index = i
# 如果假定的最小值下标发生了变化,那么就进行交换
if min_index != j:
lis[min_index], lis[j] = lis[j], lis[min_index]
if __name__ == "__main__":
lis = [2, 7, 3, 6, 9, 4]
select_sort(lis)
print(lis)
总结:
- 选择排序是不稳定的
- 最坏时间复杂度为O(n^2)
- 最优时间复杂度为O(n^2)
堆排序是指利用堆这种数据结构所设计的一种排序算法
其核心思想是:
堆排序有以下两种:
arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
,基于大根堆的堆排序得到递增序列arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
,基于小根堆的堆排序得到递减序列注意:
- 如果结点按层序从0开始编号,结点 i 的左孩子为:2i+1,结点 i 的右孩子为:2i+2,结点 i 的父结点为:(i-1)/2
- 如果结点按层序从0开始编号,假设共有 n 个结点,若 i <= n/2-1 ,该结点为非终端结点,若 i > n/2-1 ,该结点为终端结点
以大根堆为例,小根堆同理
def adjust(arr, parent, length):
# parent:父结点的索引,length:参与调整的数组长度(结点个数)
# 左孩子的索引
child = parent * 2 + 1
while child < length:
# 如果右孩子存在,且右孩子大于左孩子
if child + 1 < length and arr[child + 1] > arr[child]:
child += 1 # child变成右孩子的索引
# 父结点的值小于左、右孩子,交换
if arr[parent] < arr[child]:
arr[parent], arr[child] = arr[child], arr[parent]
parent = child
# 此时,temp和child索引都指向了子结点与父结点交换后,原来的父结点应该插入的位置(原来的子结点的位置)
# 但是我们不确定这是不是这个结点的最终位置,也就是不确定原来的父元素(小元素)往下调整时会不会破坏下面的大根堆结构
child = parent * 2 + 1 # 原来的子结点变成了父结点,再找它的左孩子,如果存在左孩子继续循环,调整根堆
else: # 父结点的值大于等于左、右孩子,不交换
break
def sort(arr):
# 从最后一个非终端结点【索引len(arr) // 2 - 1】开始向前遍历到根结点【索引0】
for i in range(len(arr) // 2 - 1, -1, -1):
adjust(arr, i, len(arr))
# i:待排序列中最后一个元素的索引
# 最后一个元素分别是从最后一个结点【索引len(arr) - 1】开始向前遍历到的第二个结点【索引1】
for i in range(len(arr) - 1, 0, -1):
# 堆顶和最后一个元素互换位置
arr[0], arr[i] = arr[i], arr[0]
# 从顶开始重新调整堆
adjust(arr, 0, i)
return arr
if __name__ == "__main__":
arr = [53, 17, 78, 9, 45, 65, 87, 32]
print(sort(arr))
总结:
- 堆排序是不稳定的
- 建堆的时间复杂度为:O(n),排序的时间复杂度为:O(nlogn),总的时间复杂度为:O(nlogn)
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序
插入算法把要排序的数组分成两部分:
def insert_sort(lis):
n = len(lis)
# 控制比较的轮数,即无序数据的个数,一个数肯定是有序的,不用比较
for j in range(1, n):
# 控制每一轮的比较次数
# i取值范围[j,j-1,j-2,j-3,,,1]
# 取出无序部分的首个,在有序部分从后向前比较,插入到合适的位置
for i in range(j, 0, -1):
# 找到合适的位置安放无序数据
if lis[i] < lis[i - 1]:
lis[i], lis[i - 1] = lis[i - 1], lis[i]
else:
break
if __name__ == "__main__":
lis = [2, 7, 3, 6, 9, 4]
insert_sort(lis)
print(lis)
总结:
- 直接插入排序是稳定的
- 最坏时间复杂度为O(n^2),本身倒序
- 最优时间复杂度为O(n),本身有序,每一轮只需比较一次
二分查找的适用前提:必须有序
非递归方法
def binary_search(lis, num):
left = 0
right = len(arr) - 1
while left <= right:
mid = (left + right) // 2
if num > lis[mid]:
left = mid + 1
elif num < lis[mid]:
right = mid - 1
else: # num == arr[mid]
return mid
return -1
if __name__ == "__main__":
lis = [1, 3, 5, 7, 9, 10]
print(binary_search(lis, 5)) # 2
print(binary_search(lis, 8)) # -1
递归方法
def binary_search(alist, left, right, item):
while left <= right:
mid = (left + right) // 2 # 获取有序数组中间值下标索引
if item < alist[mid]:
return binary_search(alist, left, mid - 1, item)
elif item > alist[mid]:
return binary_search(alist, mid + 1, right, item)
else:
return mid
return -1
if __name__ == "__main__":
lis = [1, 3, 5, 7, 9, 10]
print(binary_search(lis, 0, len(lis) - 1, 9)) # 4
print(binary_search(lis, 0, len(lis) - 1, 100)) # -1
注意:递归调用函数自身的时候,前面加上return