Python基础学习笔记 —— 数据结构与算法
创始人
2024-05-25 22:44:45
0

数据结构与算法

  • 1 数据结构基础
    • 1.1 数组
    • 1.2 链表
    • 1.3 队列
    • 1.4 栈
    • 1.5 二叉树
  • 2 排序算法
    • 2.1 冒泡排序
    • 2.2 快速排序
    • 2.3 (简单)选择排序
    • 2.4 堆排序
    • 2.5 (直接)插入排序
  • 3 查找
    • 3.1 二分查找

1 数据结构基础

本章所需相关基础知识:

  • Python基础学习笔记(二)—— 数据类型及操作
  • Python基础学习笔记(六)—— 面向对象编程(1)

self 可以看成当前类的一个对象,可以调用这个类里的函数和变量

1.1 数组

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.2 链表

1. 链表的基本结构

链表主要包括单向链表和双向链表,这是一种无须在内存中顺序存储即可保持数据之间逻辑关系的数据结构。

链表是由一个个结点(Node)连接而成的,每个结点都是包含数据域(Data)和指针域(Next)的基本单元。其基本元素如下:

  • 链表结点:每个结点分为两部分,即数据域和指针域
    • 数据域:数据域内一般存储的是整型、浮点型等数字类型
    • 指针域:指针域内一般存储的是下一个结点所在的内存空间地址
  • 头结点:指向链表的第一个结点
  • 尾结点:指向链表的最后一个结点
  • None:链表的最后一个结点的指针域,为空

单链表

单链表的每个结点的指针域只指向下一个结点,整个链表是无环的

在这里插入图片描述

双向链表

在这里插入图片描述

单向循环链表

在这里插入图片描述

相比于数组,在链表中执行插入、删除等操作可以使得操作效率大大提高。

以单链表为例,在数组内如想要删除或者插入元素到某一位置,该位置之后的所有元素都需要象前或者向后移动,这样一来,时间复杂度就与数组的长度有关,为O(n);但是在单链表中,仅仅需要通过改变所要删除或者插入位置前后结点的指针域即可,时间复杂度为O(1)。

2. 单链表的实现与基本操作

# 链表结点
class Node(object):def __init__(self, item):self.item = itemself.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 Trueelse:return False# 获取链表长度def length(self):cur = self.headcount = 0while cur is not None:cur = cur.nextcount += 1return count# 遍历链表def travel(self):cur = self.headwhile cur is not None:print(cur.item)cur = cur.next# 链表头部增加结点def add(self, item):node = SingNode(item)node.next = self.headself.head = node# 链表尾部增加结点"""注意:如果链表为空链表,cur是没有next的,只需self.head=node"""def append(self, item):node = SingNode(item)if self.is_empty():self.head = nodeelse:cur = self.headwhile cur.next is not None:cur = cur.nextcur.next = node# 链表指定位置增加结点def insert(self, pos, item):if pos == 0:self.add(item)elif pos >= self.length():self.append(item)else:node = SingNode(item)cur = self.headcount = 0while count < pos - 1:cur = cur.nextcount += 1node.next = cur.nextcur.next = node# 删除结点def remove(self, item):cur = self.headpre = Nonewhile cur is not None:# 找到了要删除的元素if cur.item == item:# 如果要删除的位置在头部if cur == self.head:self.head = cur.next# 要删除的位置不在头部else:pre.next = cur.nextreturn  # 删除元素后及时退出循环# 没有找到要删除的元素else:pre = curcur = cur.next# 查找结点def search(self, item):cur = self.headwhile cur is not None:if cur.item == item:return Truecur = cur.nextreturn False

1.3 队列

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.4 栈

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.5 二叉树

1. 树

树是一种数据结构,它是由 n 个有限结点组成的一个具有层次关系的集合。

树的基本性质如下:

在这里插入图片描述

2. 二叉树的基本结构

二叉树则是每个结点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。

二叉树的一般性质:

  • 二叉树是有序的(左右子树不能颠倒)
  • 二叉树的第 k 层上的结点数目最多为 2k−12^{k-1}2k−1
  • 深度为 h 的二叉树最多有 2h−12^h-12h−1 个结点
  • 设非空二叉树中度为0、1 和 2 的结点个数分别为n0n_0n0​ 、n1n_1n1​ 和 n2n_2n2​,则 n0=n2+1n_0 = n_2+1n0​=n2​+1
    (叶子结点比二分支结点多一个)

其他常见的二叉树:

在这里插入图片描述

注意:

  • 如果按层序从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 = itemself.lchild = Noneself.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 = nodereturn# 二叉树不空queue = []queue.append(self.root)# 编译环境会提示,也可以直接写成:queue = [self.root]while True:# 从队头取出数据node1 = queue.pop(0)# 判断左结点是否为空if node1.lchild is None:node1.lchild = nodereturnelse:queue.append(node1.lchild)# 判断右结点是否为空if node1.rchild is None:node1.rchild = nodereturnelse:queue.append(node1.rchild)# 广度优先遍历,也叫层次遍历def breadth(self):if self.root is None:returnqueue = []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 None# 前序遍历(根左右)print(tree.preorder_travel(tree.root))  # 1 2 4 3 None# # 中序遍历(左根右)print(tree.inorder_travel(tree.root))  # 4 2 1 3 None# # 后序遍历(左右根)print(tree.postorder_travel(tree.root))  # 4 2 3 1 None

注意:

  • 广度优先遍历基于队列
  • 深度优先遍历基于栈

试试 LeetCode 相关题目吧

  • 102. 二叉树的层序遍历
  • 107. 二叉树的层序遍历 II
  • 144.二叉树的前序遍历
  • 94. 二叉树的中序遍历
  • 145. 二叉树的后序遍历
  • 589. N 叉树的前序遍历
  • 590. N 叉树的后序遍历
  • 429. N 叉树的层序遍历

注意:二叉树遍历相关的题目在LeetCode环境中,省略了添加结点的代码逻辑,以及定义二叉树类中的初始化,要注意区分、根据具体题目应变。另外,其输入输出都是列表的形式,要注意

4. 由遍历结果反推二叉树结构

在这里插入图片描述

2 排序算法

算法的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变则称这种排序算法是稳定的,否则称为不稳定的

  • 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
  • 稳定的排序算法:冒泡排序、插入排序、归并排序、基数排序

2.1 冒泡排序

对要进行排序的数据中相邻的数据进行两两比较,将较大的数据放在后面,依次对所有的数据进行操作,直至所有数据按要求完成排序

如果有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:breakif __name__ == "__main__":lis = [2, 7, 3, 6, 9, 4]bubble_sort(lis)print(lis)

总结:

  • 冒泡排序是稳定的
  • 最坏时间复杂度为O(n2)O(n^2)O(n2)
  • 最优时间复杂度为O(n)O(n)O(n),遍历一遍发现没有任何元素发生了位置交换终止排序

2.2 快速排序

快速排序算法中,每一次递归时以第一个数为基准数 ,找到数组中所有比基准数小的。再找到所有比基准数大的。小的全部放左边,大的全部放右边,确定基准数的正确位置。

def quick_sort(lis, left, right):# 递归的结束条件:left > rightif left > right:return# 存储临时变量,left0始终为0,right0始终为len(lis)-1left0 = leftright0 = right# 基准值base = lis[left0]# left != rightwhile left != right:# 从右边开始找寻小于mid的值while lis[right] >= base and left < right:right -= 1# 从左边开始找寻大于mid的值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(nlog2n)O(nlog_2n)O(nlog2​n),初始序列大小均匀,每一次选择的基准值将待排序的序列划分为均匀的两部分,递归深度最小,算法效率最高
  • 最坏的时间复杂度:O(n2)O(n^2)O(n2),初始序列有序或逆序,每次选择的基准值都是靠边的元素,递归深度最大,算法效率最低

2.3 (简单)选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾,以此类推,直到全部待排序的数据元素的个数为零。

在这里插入图片描述

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)

2.4 堆排序


总结:

  • 堆排序是不稳定的
  • 建堆的时间复杂度为:O(n),排序的时间复杂度为:O(nlogn),总的时间复杂度为:O(nlogn)

2.5 (直接)插入排序

插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序

插入算法把要排序的数组分成两部分:

  • 第一部分是有序的数字(这里可以默认数组第一个数字为有序的第一部分)
  • 第二部分为无序的数字(这里除了第一个数字以外剩余的数字可以认为是无序的第二部分)
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:breakif __name__ == "__main__":lis = [2, 7, 3, 6, 9, 4]insert_sort(lis)print(lis)

总结:

  • 直接插入排序是稳定的
  • 最坏时间复杂度为O(n^2),本身倒序
  • 最优时间复杂度为O(n),本身有序,每一轮只需比较一次

3 查找

3.1 二分查找

二分查找的适用前提:必须有序

非递归方法

def binary_search(lis, num):left = 0right = len(arr) - 1while left <= right:mid = (left + right) // 2if num > lis[mid]:left = mid + 1elif num < lis[mid]:right = mid - 1else:  # num == arr[mid]return midreturn -1if __name__ == "__main__":lis = [1, 3, 5, 7, 9, 10]print(binary_search(lis, 5))  # 2print(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 midreturn -1if __name__ == "__main__":lis = [1, 3, 5, 7, 9, 10]print(binary_search(lis, 0, len(lis) - 1, 9))  # 4print(binary_search(lis, 0, len(lis) - 1, 100))  # -1

注意:递归调用函数自身的时候,前面加上return

相关内容

热门资讯

读什么什么有感的英文 读什么什么有感的英文英语读后感标题 “读XXX有感”用英语说是 “Reading after XX...
秦岚个人资料身高体重 秦岚个人资料身高体重身高:165公分 体重:46公斤秦岚 生日:七月十七日 星座:巨蟹座 出生地:沈...
双鱼和天秤会纠缠一辈子,既相配... 双鱼和天秤会纠缠一辈子,既相配又相克,为什么?双鱼座的人和天秤座的人都是比较细心的,而且特别敏感,有...
独自一人在外怎样和别人相处? 独自一人在外怎样和别人相处?我觉得独自一个人在外面一定要好好的照顾自己,应该找一份工作,找一个住的地...
朱自清散文集有哪些写的好,值得... 朱自清散文集有哪些写的好,值得背诵的?《背影》、《 春》、《 荷塘月色》、《 匆匆》都是不错的佳...
大家最讨厌的电视剧的哪一个主角... 大家最讨厌的电视剧的哪一个主角?我觉得最讨厌的电视剧主角是容嬷嬷。都挺好,里面的苏大强就是越看越别扭...
69DT伤害怎么才能上1300... 69DT伤害怎么才能上1300 !我加点是4L1M!现在60了!伤害才800!我没大号,想买梦幻币买...
孩子上课不认真听讲 孩子上课不认真听讲我的孩子七周半,已经上二年级了,但是上课不认真听讲总是搞小动作,说了很多次也不听,...
《红脸儿》的主要内容 《红脸儿》的主要内容  红脸儿主要内容:   小说以散淡而富有诗意的语言回顾了“我”与3个小伙伴之间...
异地恋的成功例子 异地恋的成功例子 情侣异地恋8年终成正果 两人存下186张火车票见证爱情一对河南的情侣在大学恋爱时便...
小狗吃了死耗子怎么办 小狗吃了死耗子怎么办你好,没事的,放心吧,你的小狗是宠物狗还是土狗,若是宠物狗的话可能会给它造成身体...
请问有没有死亡万花筒广播剧资源... 请问有没有死亡万花筒广播剧资源?死亡万花筒,我有呀!死亡万花筒广播剧,地·址:9525.video(...
徐缺有哪些女人 徐缺有哪些女人徐缺是小说《最强反套路系统》中的角色,他有许多女性关系,其中包括:1. 林小红:徐缺的...
假如我是四大名著中的人物作文9... 假如我是四大名著中的人物作文900假如你是的林黛玉的话那你就会好好读书,不至于连900个字都写不出了...
西游记81难? 西游记81难?西游记的81难是师徒四人取经回来在河中落水经书被淹了的事
魔兽世界风暴王子问题! 魔兽世界风暴王子问题!现在3.05这版本 王子第4阶段的屏障 是不是可以被MS驱散? 屏障驱散后是...
如何评价张杰的少年中国说 如何评价张杰的少年中国说我觉得非常棒,张杰的家庭条件不好。从小就非常努力。刻苦学习音乐,经过拼搏奋斗...
智取生辰纲中杨志是怎样的人?他... 智取生辰纲中杨志是怎样的人?他失败的原因是什么?简短些志有智慧,但是他忽略了一个重要的因素:团队的合...
有一本书,名字忘记了.好象是美... 有一本书,名字忘记了.好象是美国人写的.梭罗《瓦尔登湖》 如果你用的是新教材,应该是这篇吧是 海明威...
四岁孩子看什么书 四岁孩子看什么书可以看一些带有简单数字的书、色彩鲜艳的图画、动物图画等,培养他的数字感和色彩感,尽量...