数据结构与算法总结(python)

记录数据结构与算法的知识点以及常见题目,为以后复习做准备;

什么是数据结构与算法呢?
答:
算法:一系列程序指令,用以解决特定的运算和逻辑问题。
数据结构:数据结构就是一种存储和管理数据的逻辑结构。

1. 数据结构类型

数据结构类型非为:线性非线性
线性:数组、链表、堆栈、队列
非线性:树、图

1.1 数组

数组:使用一组连续的内存空间,来存储一组具有相同类型的数据;
数组特性: 查找元素快,中间插入/删除元素慢
常见题目:

1.2 链表

链表:使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。
链表特性:查找速度慢,中间插入\删除元素快
常见技巧

  1. 哑巴节点(在head之前添加一个节点,方便对链表进行从头开始处理,比如删掉头结点)
  2. 快慢指针(解决寻找中心点和回环问题)
  3. 哑巴节点+双指针

常见例题:

class Solution:
    def ReverseList(self , head: ListNode) -> ListNode:
        # 1. cur为原链表
        # 2. pre为新链表,维护新链表的内容
        # 2. temp暂存cur与cur.next断开时的cur.next剩下的原链表的顺序,
        # 保持拼接
        cur = head
        # 保持顺序
        pre = None
        while cur:
            temp = cur.next
            # 反向拼接到pre上
            cur.next = pre
            # pre更新到新增加的位置
            pre = cur
            # cur 回到原始序列继续
            cur = temp
        return pre

1.3 堆栈

堆栈:一种只允许在表的一端进行插入和删除操作的线性表:
堆栈特性:后进先出
常见例题:

1.4 队列

队列:一种只允许在表的一端进行插入操作,而在表的另一端i进行删除操作的线性表。
队列特性:先进先出
常见例题:

  • 设计循环队列
  • 用两个队列实现一个栈
  • 双端队列用于滑动窗口
  • 优先队列用于寻求TOP-K(常考)

1.6 哈希表

哈希表:也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。也就是说,它通过键 key 和一个映射函数 Hash(key) 计算出对应的值 value,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做「哈希函数(散列函数)」,存放记录的数组叫做「哈希表(散列表)」。
哈希特性:搜索速度快,但是存在哈希冲突(用链表存储可以解决)
常见题目
1. 两数之和
217. 存在重复元素
219. 存在重复元素 II
220. 存在重复元素 III

1.5 树

二叉树: 完全二叉树、完美二叉树、平衡二叉树AVL树、二叉搜索树BST
完全二叉树:堆heap、大顶堆和小顶堆
平衡二叉树AVL树:任何节点的左右子树的深度之差都不超过1.
二叉搜索树BST:左子树上的所有节点的值均比根节点小,右子树上的所有节点的值均比根节点大;中序遍历为单调序列。
常考的题目

  • 前序遍历(用宽度优先算法解决)
  • 中序遍历(用宽度优先算法解决)
  • 后序遍历 (用宽度优先算法解决)
  • 层次遍历(用广度优先算法解决)

1.6 图

:由顶点与边构成的结构;
按照是否有方向:分为有向图和无向图
按照是否由环:分为环形图和无环图
按照边是否有权重:分为环形图和无环图
涉及到:
1)最短路径:迪杰斯特拉算(Dijkstra)
2)最小生成树:Prim算法、Kruskal算法、Boruvka算法
3)并查集UnionFind:用于划分集合,数据之间的关联。包括:初始化unionCreate、路径压缩find、合并两个集合unionTwo和判断两个数据是否属于同一个集合isConnected。

2. 基础算法

2.1 排序

在这里插入图片描述

在这里插入图片描述
名词解释:
n:数据规模
k:"桶"的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

关于稳定性:
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

常考的排序算法
掌握排序的复杂度,以及常用见的冒泡排序选择排序插入排序快速排序归并排序堆排序

# 假设n个元素待排序
# 1. 冒泡排序的步骤
# 1.1 从头到开始遍历列表进行交换元素
# 比较相邻两个元素,如果前面元素比后面元素大,则交换两个元素位置,将大的元素放到后面
# 从开始第一对元素到最后一对元素进行交换,最后的元素会使最大的数
# 1.2 重复第一步骤,总要进行n-1次;
def bubble_sort(arr):
    # 遍历的次数
    for i in range(1,len(arr)):
    # 从头遍历到未进入排序的位置
        for j in range(0,len(arr)-i):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr


if __name__ == '__main__':
    s = [9,8,6,7,4,3,99,5,3]
    new_s = bubble_sort(s)
    print(new_s)
# 稳定性:稳定
# 最优时间复杂度:O(n^2)
# 最坏时间复杂度:O(n^2)
# 假设n个元素待排序
# 1. 选择排序步骤
# 1.1 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
# 1.2 再从剩余未排序元素中继续寻找着最小(大)元素,然后放到已排序的末尾。
# 1.3 重复第二步,直到所有元素排序完毕
def select_sort(arr):
    # 待插入的位置
    for i in range(0,len(arr)):
    # 从待排序位置查找最小(大)值放到待插入的位置
        min_index = i # 记录最小数的索引
        for j in range(i+1, len(arr)):
            if arr[j] < arr[min_index]:
                min_index = j
        # i不是最小数时,将i和最小数进行交换
        if i != min_index:
            arr[i], arr[min_index] = arr[min_index], arr[i]
    return arr
if __name__ == '__main__':
    s = [9,8,6,7,4,3,99,5,3]
    new_s = select_sort(s)
    print(new_s)
# 稳定性:稳定
# 最优时间复杂度:O(n^2)
# 最坏时间复杂度:O(n^2)
# 假设n个元素待排序
# 1. 插入排序步骤
# 1.1 将第一个元素看作有序序列,把第二个元素到最后一个元素当成是未排序序列
# 1.2 从未排序的初始位置开始扫描到结尾,将未排序的元素插入到有序序列的适当位置。
def insert_sort(arr):
    # 待插入的元素
    for i in range(len(arr)):
        preIndex = i - 1  # 已经排好序的最后一个位置
        current = arr[i]  # 存储待插入的元素
        while preIndex >= 0 and arr[preIndex] > current:
            arr[preIndex+1] = arr[preIndex]  # 将元素向后移
            preIndex -= 1
        arr[preIndex+1] = current

    return arr

if __name__ == '__main__':
    s = [9,8,6,7,4,3,99,5,3]
    new_s = insert_sort(s)
    print(new_s)
# 稳定性:稳定
# 最优时间复杂度:O(n^2)
# 最坏时间复杂度:O(n^2)

# 快速排序
def quick_sort(s, l, r):
    if l >= r:
        return
    # 列表的最后一个元素,s[l]作为基准值
    pivot = s[l]
    left = l
    right = r
    # 一轮循环,有可能,不能将所有的大数都放到基准值的右边,小数放到基准值的左边,所以直到left>right 跳出循环;
    while left < right:
        # 找寻右边数列比基准值小的数的位置
        while left < right and s[right] >= pivot:
            right -= 1
        # 此时有两种情况,第一种:left=right,下面操作可以说无意义;
        # 第二种:找到了s[right] >= pivot 且 left<right,意义将较大或相等的值放到左边的坑位
        s[left] = s[right]
        # 找寻左边数列比基准值大的数的位置
        while left < right and s[left] < pivot:
            left += 1
        #  此时有两种情况,第一种:left=right,下面操作可以说无意义;
        #  第二种:找到了s[right] < pivot 且 left<right,意义将较小的值放到右边的坑位
        s[right] = s[left]
        # 将大的数放在基准值的右边,小的数放在基准值的左边
    # while结束时候,left=right ,将基准值放到中间
    s[left] = pivot
    quick_sort(s, l, left-1)
    quick_sort(s, left+1, r)

if __name__ == '__main__':
    s = [9,8,6,7,4,3,99,5,3]
    quick_sort(s,0,len(s)-1)
    print(s)

# 稳定性:不稳定
# 最优时间复杂度:O(nlogn)
# 最坏时间复杂度:O(n^2)
# 归并排序
def merge(L_list, R_list):    # 拼接
    # 记录左右列表中元素位置情况
    i, j = 0,0
    res = []
    while i<len(L_list) and j <len(R_list):
        if L_list[i] < R_list[j]:
            res.append(L_list[i])
            i += 1
        else:
            res.append(R_list[j])
            j += 1
    # 两个列表中存在未合并完的数据
    res += L_list[i:] if i < len(L_list) else R_list[j:]
    return res


def merge_sort(lis):  # 分离
    length = len(lis)
    # 将列表拆分到只有一个元素为止
    if length <= 1:
        return lis
    else:
        mid = length // 2
        left = merge_sort(lis[:mid])
        right = merge_sort(lis[mid:])
        return merge(left,right)



if __name__ == '__main__':
    s = [9,8,6,7,4,3,99,5,3]
    new_s = merge_sort(s)
    print(new_s)

2.2 查找

主要是有序数组进行二分查找双指针

2.3 搜索

主要有深度优先搜索(DFS)和广度优先搜索(BFS):
经典BFS求最短路径的经典案例模板为:
二进制矩阵中的最短路径
思路:利用BFS一层一层进行所有方向的遍历,当某一层遇到目标值则停止搜索并返回当前位置:

# BFS
class Solution:
    def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
        def bfs(grid,start_x,start_y,target_x,target_y):
            m = len(grid)
            n = len(grid[0])
            queue = [] # 声明队列
            queue.append((start_x,start_y)) # 将初始点加入队列
            visited = set()  # 用于存储拜访过的点
            visited.add((start_x,start_y))
            # 从(0,0)第一步开始
            steps = 1  
            dic = [(0,1),(0,-1),(1,0),(-1,0),(-1,-1),(-1,1),(1,-1),(1,1)] # 四个移动方向
            while queue: # 队列不为空时
                # 当前节点内容大小
                size = len(queue)  
                for _ in range(size):
                    cur_x,cur_y = queue.pop(0)
                    # 如果当前点与目标点相同,返回当前步伐
                    if cur_x == target_x and cur_y == target_y:
                        return steps
                    # 遍历当前点,下个方向所有能走的点
                    for x,y in dic:
                        pre_x = cur_x + x
                        pre_y = cur_y + y
                        if 0<= pre_x < m and 0<= pre_y < n and grid[pre_x][pre_y] != 1 and (pre_x,pre_y) not in visited:
                            queue.append((pre_x,pre_y))
                            visited.add((pre_x,pre_y))
                # 一层一层寻找目标位置,当没有找到目标位置,步伐+1,如果找到上面就已经退出
                steps += 1
            return -1

            
         # 初始状态就走不通
        if grid[0][0] == 1:
            return -1 
        ans = bfs(grid,0,0,len(grid)-1,len(grid[0])-1)
        if ans == -1:
            return -1 
        else:
            return an		 

经典DFS求树前序、中序、后序遍历的经典案例模板为:

144. 二叉树的前序遍历
94. 二叉树的中序遍历
145. 二叉树的后序遍历

pre_ans = [] # 遍历前序遍历答案  根左右
mid_ans = [] # 存储中序遍历答案  左根右
b_ans = [] # 存储后序遍历答案    左右根
def dfs(root):
	if not root:
		return
	pre_ans.append(root.val)
	dfs(root.left)
	mid_ans.append(root.val)
	dfs(root.right)
	b_ans.append(root.val)
dfs(root)
return pre_ans # 返回前序遍历答案
# return mid_ans # 返回中序遍历答案
# return b_ans # 返回后序遍历答案

2.4 动态规划

动态规划题目特点:

  1. 计数:有多少种方式走到右下角;有多少种方法选出k个数使得和Sum。
  2. 求最值:从左上角走到右下角路径的最大数字和;最长上升子序列的长度。
  3. 求存在性: 取石子游戏,先手是否取胜;能不能选出k个数使得和是Sum;

动态规划四步解题法:

第一步:确认状态【最后一步是什么,优化成子问题】
第二步:转移方程
第三步:初始条件和边界情况
第四步:计算顺序【取决于当前i依赖i-1(从小到大)还是i+1(从大到小)】

2.4.1 案列:322. 零钱兑换

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        '''
        思路:
        第一步:确认状态
                最后一步是:最优策略中使用的最后一枚硬币是ak
                优化成子问题: 最少硬币个数凑成: 总金额 - ak
        第二步: 转移方程
                f(x) 表示当前状态最优的硬币数,x表示当前总金额
                f(x) = min(f(x-1)+1, f(x-2)+1, f(x-5)+1)
        第三步:初始条件和边界条件
                初始条件:f(0) = 0
                边界条件:金额不可能是负数,因为转移方程是求最小值,所以f(小于0) = 正无穷
        第四步: 计算顺序
                 因为已知初始状态为f(0),且金额是增加的,所以顺序是f(0)、f(1)..f(x)
        '''
        # dp[i]:i表示金额,d[i]凑金额的最优硬币个数
        # 开辟amount+1空间,多出来的1个空间是给f(0)的
        dp = [float('inf')]*(amount+1)
        # 初始化
        dp[0] = 0
        for i in range(1,amount+1):
            # 三种不同的硬币
            for j in coins:
                if i - j >= 0:
                    dp[i] = min(dp[i-j]+1,dp[i])
        # 最后输出状态时dp[-1],不存在dp[-1]依旧为正无穷
        return   dp[-1] if dp[-1] != float('inf') else -1

2.4.2 背包问题

01背包
思想

# 测试案例
'''
输入:
3 5
2 10
4 5
1 4
输出:
14
'''
# n 表示物品的个数, v表示背包的体积
n, v = map(int,input().split())
# w[i]表示i物品的体积,c[i]表示i物品的价值
w, c = [],[]
# 获取物品信息情况
for i in range(n):
    lis =list(map(int, input().split()))
    w.append(lis[0])
    c.append(lis[1])
dp = [0 for _ in range(v+1)]
# i遍历物品是否拿    
for i in range(n):
    # 从后向前跟新(滚动跟新),j为背包的容量
    for j in range(v,-1,-1):
        # 如果背包容量大于w[i]更新内容
        if j >= w[i]:
            # dp[j-w[i]] 表示 背包容量为j-w[i]时背包内的价值
            dp[j] = max(dp[j],dp[j-w[i]]+c[i])

print(max(dp))

完全背包

'''
输入:
2 6
5 10
3 1
输出:
10
'''
# n表示n个不同的物品,v表示背包的体积
n, v = map(int,input().split())
# w[i]表示物体i的体积,c[i]表示物品i的价值
w, c = [],[]
# 获取物品信息情况
for i in range(n):
    lis = list(map(int, input().split()))
    w.append(lis[0])
    c.append(lis[1])
dp = [0 for _ in range(v+1)]
for i in range(n):
    # 从前向更跟新
    for j in range(v+1):
        if j >= w[i]:
            dp[j] = max(dp[j],dp[j-w[i]]+c[i])
print(max(dp))

2.4.3 最长公共子序列

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        m, n = len(text1), len(text2)
        dp = [[0] * (n + 1) for _ in range(m+1)]
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if text1[i - 1] == text2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
        
        return dp[m][n]

2.4.4 最长回文子串

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        if n < 2:
            return s
        
        max_len = 1
        begin = 0
        # dp[i][j] 表示 s[i..j] 是否是回文串
        dp = [[False] * n for _ in range(n)]
        for i in range(n):
            dp[i][i] = True
        
        # 递推开始
        # 先枚举子串长度
        for L in range(2, n + 1):
            # 枚举左边界,左边界的上限设置可以宽松一些
            for i in range(n):
                # 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                j = L + i - 1
                # 如果右边界越界,就可以退出当前循环
                if j >= n:
                    break
                    
                if s[i] != s[j]:
                    dp[i][j] = False 
                else:
                    if j - i < 3:
                        dp[i][j] = True
                    else:
                        dp[i][j] = dp[i + 1][j - 1]
                
                # 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                if dp[i][j] and j - i + 1 > max_len:
                    max_len = j - i + 1
                    begin = i
        return s[begin:begin + max_len]

2.4.5 案例: 70. 爬楼梯

class Solution:
    def climbStairs(self, n: int) -> int:
        '''
        * 动态规划四部曲:
        * 1.确定dp[i]的下标以及dp值的含义: 爬到第i层楼梯,有dp[i]种方法;
        * 2.确定动态规划的递推公式:dp[i] = dp[i-1] + dp[i-2];
        * 3.dp数组的初始化:因为提示中,1<=n<=45 所以初始化值,dp[1] = 1, dp[2] = 2;
        * 4.确定遍历顺序:分析递推公式可知当前值依赖前两个值来确定,所以递推顺序应该是从前往后;
        解释为什么dp[i] = dp[i-1] + dp[i-2]:以1为结尾或以为2结尾时,前面不管怎么走,
        这两种情况都不会出现相同的状态,所以要累加;
        '''
        if n <= 2:
            return n
        # 定义范围
        dp = [0]*(n+1)
        # 初始值
        dp[1],dp[2]=1,2
        # 迭代
        for i in range(3,n+1):
            dp[i] = dp[i-1] + dp[i-2]
        return dp[-1]

参考

https://blog.csdn.net/wbzhang233/article/details/108890956
https://algo.itcharge.cn/00.Introduction/05.Categories-List/
https://blog.51cto.com/maxiaobian/3017516
https://www.runoob.com/w3cnote/ten-sorting-algorithm.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值