python数据结构与算法课程

0.浅谈算法

(1)算法定义

  • 算法(algorithm)是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性。
  • 1.问题是明确的,包含清晰的输入和输出定义。
  • 2.具有可行性,能够在有限步骤、时间和内存空间下完成。
  • 3.各步骤都有确定的含义,在相同的输入和运行条件下,输出始终相同。

(2)生活中常见的算法

例一:查字典
  • 铺垫:在字典里,每个汉字都对应一个拼音,而字典是按照拼音字母顺序排列的。假设我们需要查找一个拼音首字母为 r 的字,通常会按照如图所示的方式实现。
    在这里插入图片描述
  • 具体做法:
  • 1.翻开字典大约一半的页数,查看该页的首字母是什么,假设首字母为m。
  • 2.因为字母表中,r位于m后面,所以排除字典前半部分,查找范围缩小到后半部分。
  • 3.不断重复步骤1和步骤2的过程,直到找到字母为r的页码为止。
  • 总结:查字典这个小学生必备技能,实际上就是著名的“二分查找”算法。从数据结构的角度,我们可以把字典视为一个已排序的“数组”;从算法的角度,我们可以将上述查字典的一系列操作看作“二分查找”。
例二:整理扑克
  • 铺垫:我们在打牌时,每局都需要整理手中的扑克牌,使其从小到大排列。如图所示:

在这里插入图片描述

  • 具体做法:
  • 1.先摸上来第一张牌,此时无需做任何比较,直接放入。
  • 2.继续摸第二张牌,从第二张牌开始往前比。
  • 3.如果第二张牌比第一张牌小,则需要交换他们的位置。
  • 4.再让第三张牌和前两张牌依次比较(从第二张牌开始对比),如果第三张牌比其中任何一张牌都要小,则同样需要交换位置。
  • 5.以此类推,往后的每张牌都这样去比较然后进行排序。
  • 总结:整理扑克牌的过程本质上是“插入排序”算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都有插入排序的身影。
例三:找零钱
  • 铺垫:假设我们在超市购买了 69 元的商品,给了收银员 100 元,则收银员需要找我们 31 元。现在收银台只有面额是1 元、5 元、10 元、20 元这四种,如何尽可能用大面额的货币去完成零钱兑换呢?
    在这里插入图片描述

  • 具体做法:

  • 从可选项中拿出最大的 20 元,剩余 11 元。
  • 从剩余可选项中拿出最大的 10 元,剩余 1 元。
  • 从剩余可选项中拿出最大的 1 元,剩余 0 元。
  • 完成找零,方案为 1+10+20 = 31 元。
  • 总结:在以上步骤中,我们每一步都采取当前看来最好的选择(尽可能用大面额的货币),最终得到了可行的找零方案。从数据结构与算法的角度看,这种方法本质上是“贪心”算法。

(3)数据结构

数据结构(data structure)是组织和存储数据的方式,涵盖数据内容、数据之间关系和数据操作方法,它具有以下设计目标。

  • 空间占用尽量少,以节省计算机内存。
  • 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。
  • 提供简洁的数据表示和逻辑信息,以便算法高效运行。

Python 中提供了多种内置的数据结构,这些数据结构根据存储的数据类型和访问方式的不同而有所区别。以下是一些主要的 Python 数据结构:

  • 列表(List)
  • 元组(Tuple)
  • 集合(Set)
  • 字典(Dictionary)
  • 字符串(String)
  • 其他数据结构:除了上述内置的数据结构外,Python 还支持其他更复杂的数据结构,如栈(Stack)、队列(Queue)、树(Tree)、图(Graph)等。

(4)数据结构与算法的关系

数据结构与算法高度相关、紧密结合,具体表现在以下三个方面。

  • 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及操作数据的方法。
  • 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。
  • 算法通常可以基于不同的数据结构实现,但执行效率可能相差很大,选择合适的数据结构是关键。

(5)算法复杂度

<1> 时间复杂度
  • 定义:用来估计算法运行时间的一个式子。
  • 一般来讲,时间复杂度高的算法比复杂度低的算法慢。
  • 设输入数据大小为 n ,常见的时间复杂度类型如图所示(按照从低到高的顺序排列):
    在这里插入图片描述
  1. 常数阶 O(1)
  • 常数阶的操作数量与输入数据大小 n 无关,即不随着 n 的变化而变化。在以下函数中,尽管操作数量 size 可能很大,但由于其与输入数据大小 n 无关,因此时间复杂度仍为 O(1) .
def constant(n):
    """常数阶"""
    count = 0
    size = 100000
    for _ in range(size):
        count += 1
    return count
  1. 线性阶 O(n)
  • 线性阶的操作数量相对于输入数据大小 n 以线性级别增长。线性阶通常出现在单层循环中,例如遍历数组的时间复杂度为 O(n) ,其中 n 为数组或链表的长度:
def array_traversal(nums)
    """线性阶(遍历数组)"""
    count = 0
    # 循环次数与数组长度成正比
    for num in nums:
        count += 1
    return count
  1. 平方阶 O(n2)
  • 平方阶的操作数量相对于输入数据大小 n 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环的时间复杂度都为 O(n) ,因此总体的时间复杂度为 O(n22).
def quadratic(n)
    """平方阶"""
    count = 0
    # 循环次数与数据大小 n 成平方关系
    for i in range(n):
        for j in range(n):
            count += 1
    return count
  1. 指数阶 O(2n)
  • 生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 1 个细胞,分裂一轮后变为 2 个,分裂两轮后变为 4 个,以此类推,分裂 n 轮后有 2n 个细胞。
def exponential(n)
    """指数阶(循环实现)"""
    count = 0
    base = 1
    # 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)
    for _ in range(n):
        for _ in range(base):
            count += 1
        base *= 2
    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
    return count
  1. 对数阶 O(logn)

与指数阶相反,对数阶反映了“每轮缩减到一半”的情况。设输入数据大小为 n ,由于每轮缩减到一半,因此循环次数是 O( log ⁡ 2 n \log_2n log2n) ,简记为 O(logn),典型的例子就是二分查找。

def logarithmic(n):
    """对数阶(循环实现)"""
    count = 0
    while n > 1:
        n = n / 2
        count += 1
    return count
  1. 线性对数阶 O(n*logn)

线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 O(logn) 和 O(n) , 因此时间复杂度为 O(n*logn)。例如:

def linear_log_recur(n):
    """线性对数阶"""
    if n <= 1:
        return 1
    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)
    for _ in range(n):
        count += 1
    return count

主流排序算法的时间复杂度通常为 O(n*logn)
,例如快速排序、归并排序、堆排序等。

  1. 阶乘阶 O(n!)

阶乘阶对应数学上的“全排列”问题。给定 n 个互不重复的元素,求其所有可能的排列方案,方案数量为:
n! = n * (n-1) * (n-2) * ...*2 * 1

def factorial_recur(n):
    """阶乘阶(递归实现)"""
    if n == 0:
        return 1
    count = 0
    # 从 1 个分裂出 n 个
    for _ in range(n):
        count += factorial_recur(n - 1)
    return count
<2> 空间复杂度
  • 定义:用来估计算法内存占用大小的一个式子。
  • 空间复杂度的表示方式和时间复杂度一样。
  • “空间换时间”
  1. 常数阶 O(1)

常数阶常见于数量与输入数据大小 n 无关的常量、变量、对象。

# 循环中的变量占用 O(1) 空间
for _ in range(n):
    c = 0
  1. 线性阶 O(n)

线性阶常见于元素数量与 n 成正比的数组、链表、栈、队列等:

def store_in_array(data):
    # 假设data是一个长度为n的列表
    n = len(data)
    # 创建一个新的数组来存储数据,空间复杂度为O(n)
    arr = []
    for i in data:
        arr.append(i)
    # 这里我们只是简单地将数据复制到了一个新数组,所以其他操作的空间复杂度为O(1)
    return arr

# 使用示例
data = [1, 2, 3, 4, 5]
result = store_in_array(data)
print(result)
  1. 平方阶 O(n2)

  2. 指数阶 O(2n)

  3. 对数阶 O(logn)

1.枚举算法

  • 枚举的定义:根据所需解决问题的条件,把该问题所有可能的解,一一列举出来,并逐个检验出问题真正解的方法。枚举法也称穷举法。

(1)判断水仙花数

水仙花数:指一个 n 位数(n≥3),它的每个位上的数字的 n 次幂之和等于它本身。例如,153是一个水仙花数,因为1^3 + 5^3 + 3^3 = 153。

题目:找出100~999整数中的所有水仙花数.

  • 方法一:使用while循环
num = 100
while num < 1000:
    a = num // 100
    b = num % 100 // 10
    c = num % 10
    if a**3+b**3+c**3 == num:
        print(num,'是一个水仙花数')
    num += 1
    
#提示:“//”表示整除,“/”表示除法,“%”表示取余,“**”表示幂次方
  • 方法二:使用for循环
for x in range(100,1000):
    a = int(x/100) #百位数
    b = int(x%100/10) #十位数
    c = int(x%10) #个位数
    if a**3+b**3+c**3==x:
        print(x,'是一个水仙花数')
    x+=1

结果:
在这里插入图片描述

(2)鸡兔同笼

有一个笼子,里面有鸡和兔子。我们知道总共有7个头和18只脚,我们要找出有多少只鸡和多少只兔子。

  • 解法一:假设法

先假设它们全是鸡,每少2只脚就说明有一只兔被看成了鸡;将少的脚数量除以2,就可以算出共有多少只兔,我们称这种解题方法为假设法。解题步骤:

  1. 假设全部是鸡,此时总的脚数:7x2 = 14 只
  2. 一共被看少的脚数量:18-14 = 4 只
  3. 兔子的数量:4/2 = 2 只
  4. 鸡的数量:7-2 = 5 只
  • 解法二:一元一次方程

设笼子里有 x 只鸡,那么兔子有:7-x 只。根据题目,我们可以建立以下方程:

  1. 脚的总数是 2x + 4*(7-x) = 18(鸡有2只脚,兔子有4只脚,总脚数就是2倍的鸡脚数加上4倍的兔脚数)。

  2. 现在我们要来解这个方程组,找出 x 的值。计算结果为:x = 5。所以,笼子里有 5 只鸡和 2 只兔子。

  • 解法三:一元二次方程

设笼子里有 x 只鸡和 y 只兔子。根据题目,我们可以建立以下方程:

  1. 头的总数是 x + y = 7(鸡和兔子的头数加起来)。
  2. 脚的总数是 2x + 4y = 18(鸡有2只脚,兔子有4只脚,总脚数就是2倍的鸡脚数加上4倍的兔脚数)。
  3. 现在我们要来解这个方程组,找出 x 和 y 的值。计算结果为: {x: 5, y: 2}。所以,笼子里有 5 只鸡和 2 只兔子。
  • 解法四:枚举算法
  • 以上我们用的是数学中列举方程的形式求解,我们也可以利用枚举法,通过python代码帮我们计算最终的结果。

  • 枚举的思路如图所示:一一列举,最终得到总的脚数量为18的组合,答案即为5 只鸡和 2 只兔子。

在这里插入图片描述
# 使用while循环求解
head = 7 #鸡和兔总的个数
foot = 18 #鸡和兔总的脚数量
chicken = 0
rabbit = 0
while True:
    if 2*chicken + 4*rabbit == 18:
        break   
    chicken += 1
    rabbit = head-chicken
print(chicken,rabbit) #5 2

-----------------------------------
# 也可使用for循环求解
chicken = 1
rabbit = 6
for i in range(1, 7):
    if 2*chicken + 4*rabbit == 18:
        print(chicken, rabbit)
        break
    chicken += 1
    rabbit = 7- chicken

(3)因式分解

题目:有两个两位数,他们的乘积等于1691,求这两个数分别是多少?

for i in range(10,100):
    for j in range(10,100):
        if i*j == 1691:
            print(i,j)
            break

在这里插入图片描述

思考:以上结果为何会输出两遍?代码能否进行优化呢?

代码优化:

for i in range(10,100):
    for j in range(i,100):
        if i*j == 1691:
            print(i,j)
            break

在这里插入图片描述

(4)找质数

题目:找出1到20内的所有质数

提示:质数是指大于1的自然数,除了1和它本身以外没有任何正因数(除了1和它本身外不能被其他整数整除)。换句话说,质数是只有两个正因数的数,这两个因数就是1和它自己。

for num in range(2, 21):  # 起始值为2,对于范围在2到20的每一个数字
    for i in range(2, num):  # 对于从2到num-1的每一个数字
        if num % i == 0:  # 如果num能被i整除
            break  # 退出内层循环,说明num不是质数
    else:
        print(num)  # 如果内层循环完整执行(即未中断),则说明num是质数,打印输出
# 结果:2、3、5、7、11、13、17、19

2.查找算法

(1)顺序查找

思路:

  • 遍历列表。
  • 找到跟目标值相等的元素,就返回他的下标。
  • 遍历结束后,如果没有搜索到目标值,就返回-1。

第一种写法:

list = [5, 2, 9, 1, 3, 4]
L = len(list)
num = input('请输入您要查找的数字:')
num = int(num)
b = False
for i in range(0, L):
    if num == list[i]:
        print('您查找到的数字其索引值为:', i)
        b = True
        break
if b == False:
    print('找不到该数字!')

第二种写法:

list = [5,2,9,1,3,4]
L = len(list)
num = input('请输入您要查找的数字:')
num = int(num)
for i in range(0,L):
    if num==list[i]:
        print('您查找到的数字其索引值为:',i)
        break
else:
    print('找不到该数字!')

注意:该else语句与for循环相关联,而不是与if语句相关联。如果for循环完成时没有遇到break,则意味着在列表中未找到该数字,else将会被执行。这个写法应该是 Python 特有的,与其他编程语言略有不同。

时间复杂度:O(n)

(2)二分查找

【注意】:二分查找的前提是数字是排序好的。

思路:

  • 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束。
  • 如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数组中搜索。

动画演示:

在这里插入图片描述

list = [1, 2, 3, 4, 5, 9]
L = len(list)
left = 0
right = L-1
target = 9
while left<=right: #这里的判断是left<=right;避免查找的元素处于边缘位置,而没有查找到的情况。【例如:[1,2,3,4],找目标值4】
    mid = (left+right)//2 #地板除:只保留整数
    if list[mid]<target:
        left = mid+1
    elif list[mid]>target:
        right = mid-1
    else:
        print('您查找的数字其索引值为:',mid)
        break
else:
    print('找不到该数字!')

时间复杂度:O(logn)

3. 排序算法

菜鸡三人组:

  • 冒泡排序
  • 选择排序
  • 插入排序

牛逼三人组:

  • 快速排序
  • 堆排序(难)
  • 归并排序(较难)

(1)冒泡排序

思路:

  • 1.比较所有相邻的元素,如果第一个比第二个大,则交换他们。
  • 2.一轮下来,可以保证最后一个数是最大的。
  • 3.以此类推,执行n-1轮,就可以完成排序。
  • 动画演示:

在这里插入图片描述
代码参考:

list = [6,5,4,1,3,2]
L = len(list)
def fn(list):
    for i in range(0,L-1):
        for j in range(0,L-1):
            if list[j]>list[j+1]:
                list[j],list[j+1] = list[j+1],list[j]
    return list
print(fn(list))
  • 思考:以上代码能否进行优化呢?内层循环需要每次都遍历0~len-1次吗??
  • 回答:不需要,因为随着外层循环次数的增加,数组末尾的排序会依次确定好位置,例如,第一轮会确定6的位置,第二轮会确定5的位置。所以,内层循环不需要每次都遍历0~len-1次,只需要遍历len-1-i 次就够了。

优化后的代码:

list = [6,5,4,1,3,2]
L = len(list)
def fn(list):
    for i in range(0,L-1):
        for j in range(0,L-1-i):
            if list[j]>list[j+1]:
                list[j],list[j+1] = list[j+1],list[j]
    return list
print(fn(list))

冒泡排序时间复杂度:O(n2)

(2)选择排序

思路:

  • 1.找到数组中的最小值,把他更换到列表中的第一位。(具体做法:先假设第一数为最小值,记录它的索引值,将第一数和第二个数作比较,如果第一个数大于第二个数,将最小索引值记录为第二个数,依次循环比较,一轮比较下来,最小值所在的索引位置就会被找到,并且把他更换到最开头的位置。
  • 2.接着找到第二小的值,把他更换到数组中的第二位。
  • 3.以此类推,执行n-1轮,就可以完成排序。
  • 动画演示:

在这里插入图片描述

参考代码:

list = [6,5,4,1,3,2]
L = len(list)
def fn(list):
    for i in range(0,L-1):
        min = i
        for j in range(i,L-1):
            if list[min]>list[j+1]:
                min = j+1
        list[min],list[i] = list[i],list[min]
    return list
print(fn(list))

选择排序时间复杂度:O(n2)

(3)插入排序

插入排序(insertion sort)是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。

基本思路:

  • 在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。

具体步骤:

  1. 从第二个元素开始,依次将元素插入已经排序好的部分。
  2. 遍历已排序好的部分,找到合适的插入位置。
  3. 将待排序的元素插入合适的位置。
  4. 将大于待排序元素的元素向后移动一个位置。
  5. 依次类推,完成插入排序。

例子:

  • 插入排序的总体过程,类似我们打牌,摸牌后进行依次插入的过程:
    在这里插入图片描述

  • 举例:假如已经进行到31这个数了,31前面的数我们已经插入排序完毕了;那么对于31这个数,我们需要先将其与93比较,31<93,交换位置;接着比较31<77,交换位置;接着比较31<54,交换位置;接着比较31>26,不需要交换位置了,此时内层循环可以结束了。
    在这里插入图片描述

动画演示

在这里插入图片描述

代码参考:

在这里插入图片描述

插入排序的外层循环为for循环 + 内层循环为while循环,所以时间复杂度为 O(n2)

(4)快速排序

在这里插入图片描述

  • 第一轮排序的代码:
def partition(list,left,right):
    tmp = list[left] 
    while left < right:
        while left < right and list[right] >= tmp: # 从右边找出比tmp小的数
            right -= 1  # 往左移动一步
        list[left] = list[right]  # 把右边的值给到左边的空缺位置
        print(list)
        while left < right and list[left] <= tmp:
            left += 1
        list[right] = list[left]
        print(list)
    list[left] = tmp # 将tmp进行归位

list = [5,7,4,6,3,1,2,9,8]
print(list)
partition(list, 0, len(list)-1)
print(list)

结果:
在这里插入图片描述

问题解析:: 为何是先从右往左进行查找?
答: 因为左边空出了位置, 从右往左找出比tmp小的数字, 可以放到left指针所在的位; 如果一开始从左往右查找, 右边并没有空余位置。

代码解析1: 
while left < right and list[right] >= tmp:: 以上这行代码为何添加条件判断 left < right
答: 因为如果不添加, right可能会跑到left的左边, 比如5 6 7,right指针最终会移动到5所在位置的左边

# 代码解析2: 
while left < right and list[right] >= tmp:: 以上这行代码为何是 list[right] >= tmp, 不能是list[right] > tmp:: 因为如果是list[right] > tmp, 假如列表为5 6 5 7, right指针移动到5所在位置就会停止了, 此时
列表的顺序为: 5 6 5 7, left指针也会一直停留在左边5的位置, 循环无法跳出来.
  • 添加递归函数,实现左右两边也进行快速排序最终版代码:
def quick_sort(list,left,right):
    if left < right: # 至少存在两个元素
        mid = partition(list,left,right)
        quick_sort(list,left,mid-1)
        quick_sort(list,mid+1,right)

def partition(list, left, right):
    tmp = list[left]
    while left < right:
        while left < right and list[right] >= tmp:  # 从右边找出比tmp小的数
            right -= 1  # 往左移动一步
        list[left] = list[right]  # 把右边的值给到左边的空缺位置
        while left < right and list[left] <= tmp:
            left += 1
        list[right] = list[left]
    list[left] = tmp  # 将tmp进行归位
    return left;

list = [5, 7, 4, 6, 3, 1, 2, 9, 8]
quick_sort(list, 0, len(list)-1)
print(list)


结果:

在这里插入图片描述

快速排序时间复杂度:O(nlogn)

(5)归并排序

  • 理解一次归并
    在这里插入图片描述

  • 一次归并动画演示:点我观看

  • 一次归并的代码:

def merge_one(li, low, mid, high):
    i = low
    j = mid+1
    arr = []  # 存储拿出来的元素
    while i <= mid and j <= high:  # 只要左右两边都有数
        if li[i] < li[j]:
            arr.append(li[i])
            i += 1
        else:
            arr.append(li[j])
            j += 1
    # 如果左边还有元素,全部加入列表
    while i <= mid:
        arr.append(li[i])
        i += 1
    # 如果右边还有元素,全部加入列表
    while j <= high:
        arr.append(li[j])
        j += 1

    # 将arr排序好的元素拷贝给li
    li[low:high+1]=arr

li = [2, 5, 7, 8, 9, 1, 3, 4, 6]
merge_one(li, 0, 4, 8)
print(li) #[1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 归并排序整体思路:【递归解决:先分解,再合并】
    在这里插入图片描述

  • 归并排序图解:

在这里插入图片描述

  • 归并排序最终代码:
def merge_one(li, low, mid, high):
    i = low
    j = mid+1
    arr = []  # 存储拿出来的元素
    while i <= mid and j <= high:  # 只要左右两边都有数
        if li[i] < li[j]:
            arr.append(li[i])
            i += 1
        else:
            arr.append(li[j])
            j += 1
    # 如果左边还有元素,全部加入列表
    while i <= mid:
        arr.append(li[i])
        i += 1
    # 如果右边还有元素,全部加入列表
    while j <= high:
        arr.append(li[j])
        j += 1

    # 将arr排序好的元素拷贝给li
    li[low:high+1]=arr

def merge_sort(li,low,high):
    if low<high: #至少存在两个元素,进行递归
        mid = (low+high)//2
        merge_sort(li, low, mid)
        merge_sort(li, mid+1, high)
        merge_one(li, low, mid, high)

li = [5,2,8,7,9,3,4,6,1]
merge_sort(li,0,len(li)-1)
print(li) #[1, 2, 3, 4, 5, 6, 7, 8, 9]

4.栈

  • 定义:栈是一个数据集合,可以理解为只能在一端进行插入或者删除操作的列表。
  • 特点:后进先出
  • 基本操作:
    • 进栈:append
    • 出栈:pop
    • 取栈顶:list[-1]
class Stack:
    def __init__(self):
        self.stack = []
    def append(self,element):
        self.stack.append(element)
    def pop(self):
        return self.stack.pop()
    def get_top(self):
        if len(self.stack)>0:
            return self.stack[-1]
        else:
            return None
stack = Stack()
stack.append(1)
stack.append(2)
stack.append(3)
print(stack.stack) #[1,2,3]
print(stack.pop()) #3

题目:有效的括号

5.队列

  • 队列是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
  • 进行插入的一端为队尾,插入动作称为进队或入队。
  • 进行删除的一端为队头,删除动作称为出队。
  • 队列的性质:先进先出。

队列的操作:

  1. Queue() 创建一个空的队列
  2. enqueue(item) 往队列中添加一个 item 元素
  3. dequeue() 从队列头部删除一个元素
  4. is_empty() 判断一个队列是否为空
  5. size() 返回队列的大小
class Queue(object):
    def __init__(self):
        self.items = []

    def is_empty(self):
        return self.items == []

    def enqueue(self, item): 
        """进队列"""
        self.items.append(item)

    def dequeue(self): 
        """出队列"""
        return self.items.pop(0)

    def size(self): 
        """返回大小"""
        return len(self.items)

q = Queue()
q.enqueue("hello")
q.enqueue("world")
q.enqueue("bjsxt")
print(q.dequeue())
print(q.dequeue())
print(q.dequeue())

6.链表

(1)链表的定义

在这里插入图片描述

class Node:
    def __init__(self,item):
        self.item = item
        self.next = None

a = Node(1)
b = Node(2)
c = Node(3)
a.next = b
b.next = c

print(a.next==b) #True
print(a.next.next.item) #3

(2)创建链表

在这里插入图片描述

class Node:
    def __init__(self, item):
        self.item = item
        self.next = None

# 头插法
def creat_linklist_head(li):
    head = Node(li[0])
    for element in li[1:]:
        node = Node(element)
        node.next = head
        head = node
    return head

# 尾插法
def creat_linklist_tail(li):
    head = Node(li[0])
    tail = head
    for element in li[1:]:
        node = Node(element)
        tail.next = node
        tail = node
    return head

# 打印链表
def print_linklist(lk):
    while lk:
        print(lk.item,end=" ")
        lk = lk.next

lk = creat_linklist_head([1, 2, 3])
print_linklist(lk)  # 3 2 1

lk = creat_linklist_tail([1, 2, 3])
print_linklist(lk)  # 1 2 3

(3)插入节点

  • 链表节点的插入:
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

(4)删除节点

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

(5)双链表

在这里插入图片描述

  • 双链表节点的插入:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 双链表节点的删除:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.树

在这里插入图片描述
在这里插入图片描述

7.1 二叉树

在这里插入图片描述

class BiTreeNode:
    def __init__(self,data):
        self.data = data
        self.lchild = None
        self.rchild = None

a = BiTreeNode("A")
b = BiTreeNode("B")
c = BiTreeNode("C")
d = BiTreeNode("D")
e = BiTreeNode("E")
f = BiTreeNode("F")
g = BiTreeNode("G")

e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f

root = e
print(root.lchild.rchild.data) #C
 

(1)二叉树的前中后序遍历

在这里插入图片描述

from collections import deque

class BiTreeNode:
    def __init__(self, data):
        self.data = data
        self.lchild = None
        self.rchild = None

a = BiTreeNode("A")
b = BiTreeNode("B")
c = BiTreeNode("C")
d = BiTreeNode("D")
e = BiTreeNode("E")
f = BiTreeNode("F")
g = BiTreeNode("G")

e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f

root = e

# 前序遍历
def pre_order(root):
    if root:
        print(root.data, end=" ")
        pre_order(root.lchild)
        pre_order(root.rchild)

# 中序遍历
def in_order(root):
    if root:
        in_order(root.lchild)
        print(root.data, end=" ")
        in_order(root.rchild)

# 后序遍历
def post_order(root):
    if root:
        post_order(root.lchild)
        post_order(root.rchild)
        print(root.data, end=" ")

# 层序遍历
def level_order(root):
    queue = deque()
    queue.append(root)
    while len(queue)>0:
        node = queue.popleft()
        print(node.data,end=" ")
        if node.lchild:
            queue.append(node.lchild)
        if node.rchild:
            queue.append(node.rchild)


pre_order(root)  # E A C B D G F
in_order(root)  # A B C D E G F
post_order(root)  # B D C A F G E
level_order(root)  # E A G C F B D

7.2 二叉搜索树

在这里插入图片描述

8.深度优先搜索

9.广度优先搜索

10.图

11.贪心算法

12.动态规划

(1)动态规划算法介绍

LeetCode简单的动态规划题:

LeetCode较难的动态规划题:

总结:

动态规划与其说是一个算法,不如说是一种方法论。
该方法论主要致力于将“合适”的问题拆分成三个子目标一一击破:

  • 1.建立状态转移方程
  • 2.缓存并复用以往结果
  • 3.按顺序从小往大算

(2)01背包”问题

概念:有N件物品和一个最多能装重量为W 的背包。第i件物品的重量是weight[i],价值是value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. 假如现在只有吉他(G) , 这时不管背包容量多大,只能放一个吉他1500(G) 
2. 假如有吉他和音响, 验证公式:v[1][1] =1500
	(1). i = 1, j = 1 
	(2). w[i] = w[1] = 1 j = 1   
	v[i][j]=max{
    
    v[i-1][j], v[i]+v[i-1][j-w[i]]} : 
	v[1][1] = max {
    
    v[0][1], v[1] + v[0][1-1]} = max{
    
    0, 1500 + 0} = 1500
3. 假如有吉他/音响/电脑, 验证公式:v[3][4] 
	(1). i = 3;j = 4
	(2). w[i] = w[3] =3  j = 4
	j = 4 >= w[i] = 3 => 4 >= 3
	v[3][4] = max {
    
    v[2][4], v[3] + v[2][1]} = max{
    
    3000, 2000+1500} = 2000+1500

归纳:

从表格的右下角开始回溯,如果发现前n个物品的最佳组合的价值和前n-1个物品最佳组合的价值一样,说明第n个物品没有被装入;否则,第n个物品被装入。

问题:背包容量为4时,能装入物品的最大价值是多少?

参考代码:

w = [1, 4, 3]  # 物品重量
value = [1500, 3000, 2000]  # 物品的价值
m = 4  # 背包容量
n = 3  # 物品的个数

# 初始化二维数组:4行5列
v = []
for i in range(4):
    v.append([])
    for j in range(5):
        v[i].append(0)

for i in range(1, 4):  # 先遍历物品
    for j in range(1, 5):  # 后遍历背包容量
        if w[i-1] > j:  # w[i - 1]是避免跳过第一个物品,同理else中的语句也是一样的
            v[i][j] = v[i-1][j]
        else:
            v[i][j] = max(v[i - 1][j], value[i - 1] + v[i - 1][j - w[i - 1]])
print(v)
#结果:[
#       [0, 0, 0, 0, 0],
#       [0, 1500, 1500, 1500, 1500], 
#       [0, 1500, 1500, 1500, 3000], 
#       [0, 1500, 1500, 2000, 3500]
#     ]

LeetCode中 “01背包” 题型汇总:

(3)完全背包”问题

视频参考链接

概念:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],价值是value[i]。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

w = [1, 4, 3]  # 物品重量
value = [1500, 3000, 2000]  # 物品的价值
m = 4  # 背包容量
n = 3  # 物品的个数

# 初始化二维数组:4行5列
v = []
for i in range(4):
    v.append([])
    for j in range(5):
        v[i].append(0)

for i in range(1, 4):  # 先遍历物品
    for j in range(1, 5):  # 后遍历背包容量
        for k in range(j//w[i-1]+1):
            if w[i-1] > j:  # w[i - 1]是避免跳过第一个物品,同理else中的语句也是一样的
                v[i][j] = v[i-1][j]
            else:
                v[i][j] = max(v[i - 1][j], k*value[i - 1] +
                              v[i][j - k*w[i - 1]])
print(v)
# 结果:[
#       [0, 0, 0, 0, 0],
#       [0, 1500, 3000, 4500, 6000],
#       [0, 1500, 3000, 4500, 6000],
#       [0, 1500, 3000, 4500, 6000]
#     ]

LeetCode中 “完全背包” 题型汇总:

(4) “打家劫舍”系列

(5)“股票”系列【大多可用“贪心”思维】

在这里插入图片描述

(6) “子序列”系列

以下标黄题目思路基本一致:

(7)“跳跃游戏”系列【可用“贪心”思维】

13.分治算法

14.回溯算法

猜你喜欢

转载自blog.csdn.net/m0_46403734/article/details/137517367