Python基础学习笔记 —— 数据结构与算法_数据结构与算法python笔记(1)

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

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

树的基本性质如下:

在这里插入图片描述

2. 二叉树的基本结构

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

二叉树的一般性质:

  • 二叉树是有序的(左右子树不能颠倒)
  • 二叉树的第 k 层上的结点数目最多为

2

k

1

2^{k-1}

2k−1

  • 深度为 h 的二叉树最多有

2

h

1

2^h-1

2h−1 个结点

  • 设非空二叉树中度为0、1 和 2 的结点个数分别为

n

0

n_0

n0​ 、

n

1

n_1

n1​ 和

n

2

n_2

n2​,则

n

0

=

n

2

1

n_0 = n_2+1

n0​=n2​+1(叶子结点比二分支结点多一个)

注意:这里的层数 k ,深度 h 均是从 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 = 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. 由遍历结果反推二叉树结构

在这里插入图片描述

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:
            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),遍历一遍发现没有任何元素发生了位置交换终止排序

2.2 快速排序

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

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(nlog2​n),初始序列大小均匀,每一次选择的基准值将待排序的序列划分为均匀的两部分,递归深度最小,算法效率最高

  • 最坏的时间复杂度:

O

(

n

2

)

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 堆排序

堆排序是指利用堆(必须是一种完全二叉树)这种数据结构所设计的一种排序算法

其核心思想是:

  • 建立(大或小)根堆:
    • 从最后一个非终端结点开始,把所有的非终端结点都检查一遍,是否满足(大或小)根堆的要求,如不满足,则与更(大或小)的结点进行交换,元素互换可能会破坏下一层的堆,需要采用相同的方法进行调整,得到(大或小)根堆。
  • 排序:
    • 每一趟排序将堆顶元素加入有序子序列(堆顶元素与待排序序列中最后一个元素交换),并将待排序元素序列再次调整为(大或小)根堆

堆排序有以下两种:

  • 大根堆:每个结点的值都大于等于其左右孩子结点的值,满足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)

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:
                break


if __name__ == "\_\_main\_\_":
    lis = [2, 7, 3, 6, 9, 4]
    insert_sort(lis)
    print(lis)

总结:

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

2.6 归并排序

def merge\_sort(nums):
    if len(nums) <= 1:
        return nums

    mid = len(nums) // 2
    left = merge_sort(nums[:mid])
    right = merge_sort(nums[mid:])
    return merge(left, right)

def merge(left, right):
    merged = []
    i = j = 0

    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i])
            i += 1
        else:
            merged.append(right[j])
            j += 1

    while i < len(left):
        merged.append(left[i])
        i += 1

    while j < len(right):
        merged.append(right[j])
        j += 1

    return merged

总结:

  • 归并排序是稳定的
  • 假设数组长度为 n,则在归并排序的过程中,需要进行 logn 次划分,每次划分需要 O(n) 的合并操作,因此,归并排序的总体时间复杂度为

O

(

n

l

o

g

n

)

O(nlogn)

O(nlogn)
划分过程:每次将数组划分为两个大小相等的子数组,所需时间复杂度为 O(logn)。
合并过程:对于每个子数组,需要线性时间将其合并成一个有序的数组。合并的时间复杂度为 O(n)。

  • 归并排序的空间复杂度为

O

(

n

l

o

g

n

)

=

O

(

n

)

O(n+logn)=O(n)

O(n+logn)=O(n),因为在合并过程中需要创建一个临时数组来存储合并的结果,另外归并排序递归调用的层数最深为

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

o

g

n

)

O(nlogn)

O(nlogn)
划分过程:每次将数组划分为两个大小相等的子数组,所需时间复杂度为 O(logn)。
合并过程:对于每个子数组,需要线性时间将其合并成一个有序的数组。合并的时间复杂度为 O(n)。

  • 归并排序的空间复杂度为

O

(

n

l

o

g

n

)

=

O

(

n

)

O(n+logn)=O(n)

O(n+logn)=O(n),因为在合并过程中需要创建一个临时数组来存储合并的结果,另外归并排序递归调用的层数最深为

[外链图片转存中…(img-JddxdlOi-1715332458177)]
[外链图片转存中…(img-4n79EDhy-1715332458178)]
[外链图片转存中…(img-W0rkd96C-1715332458178)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值