2019.03.12 - 常见算法和数据结构

文章目录

抽象数据类型和面向对象编程

学习资料

示例:Bag类

  • data:容器

  • method

    • add:添加
    • remove:删除
    • len:查看长度
    • iter:迭代
  • 代码示例:bag_adt.py
    在这里插入图片描述

  • 注意事项

    • 选用DataStructure
    • 能否操作 add 和 remove
    • 效率如何

数组

定长数组

  • 代码示例:array_adt.py
    在这里插入图片描述

链表

线性与链式结构

  • 线性结构:内存连续,下标访问
  • 链式结构:内存不连续,不能下标访问,添加元素方便,通过遍历来寻找元素

单链表

单链表结构

  • 根节点(Root):是入口

  • 首节点(Head):第一个节点

  • 尾节点(Tail):最后一个节点
    在这里插入图片描述

  • 节点(Node)

    • 值(value):节点内容
    • 指针(next):指向下个节点
      在这里插入图片描述
LinkedList代码结构
  • LinkedList

    • data
      • root, length, tail_node
    • method
      • init, is_empty, __len__, append, appendleft, iter_node, __iter__, remove, find, popleft, clear, reverse
  • 代码示例:linked_list_adt.py

delete操作示意图

在这里插入图片描述

各操作的时间复杂度

在这里插入图片描述

单链表反转

LinkedList.reverse()

方案1:利用append_left
  • 遍历节点,并把元素从首位插入
    在这里插入图片描述
方案2:前驱与后继互换
  • 遍历节点
  • 每个节点指向其前驱
  • 记得更新尾节点和首节点
    在这里插入图片描述
    在这里插入图片描述

双链表

  • 双链表又称双端链表
  • 优化了单链表查询的低效率

双链表结构

  • 节点(Node)
    • 值(value):节点内容
    • 前驱指针(prev):指向上个节点
    • 后继指针(next):指向下个节点
      在这里插入图片描述
循环双端链表
  • 根节点的前驱指针 指向 尾节点
    在这里插入图片描述
CyclicDoubleLinkedList代码结构
  • LinkedList

    • data
      • root, maxsize, length
    • method
      • headnode, tailnode, append, append_left, remove(O(1)), iter_node, iter_node_reverse
  • 代码实现:略

时间复杂度

常见算法的时间复杂度

在这里插入图片描述

空间复杂度

常见数据结构的复杂度

在这里插入图片描述

常见结构与算法的时间复杂度

在这里插入图片描述

常见复杂度增长趋势

在这里插入图片描述

  • 时间换空间,空间换时间

    • 栈区
    • 先进后出(last in first out)

栈结构

在这里插入图片描述

Stack代码结构

  • Stack

    • data
    • method
      • push, pop, pop_left
  • 代码实现:stack_adt.py

    • 基于list实现
  • 思考题:利用双端队列实现

    • 基于collections.deque实现
collections.deque

python内置的 双端队列

  • append, appendleft:追加元素,从队首加入元素
  • extend, extendleft:拼接入元素
  • maxlen:字段,最大长度
  • pop, popleft
  • remove
  • clear:清空元素
  • reverse:逆序
  • rotate:把最右侧的元素放到最左侧(循环)

队列

队列结构

在这里插入图片描述

  • 特点:先入先出(FIFO),有序,可能有maxSize。
  • 实现方式:
    1. 数组/队列:进出元素可能需要挪动队列中的所有元素,效率差。【优化:环形数组】
    2. 链表。

Python实现

  • 直接使用list即可实现。【最差方式】
class Queue:
    """队尾进,队首出"""
    def __init__(self):
        self._queue = []

    def add(self, value):
        self._queue.append(value)

    def out(self):
        return self._queue.pop(0)

golang实现

参考:尚硅谷-数组模拟队列

数组模拟队列
  • 使用head和tail分别记录 队首(队首前一个) 和 队尾。

  • head随着输出变化,tail随着输入变化。
    在这里插入图片描述

  • 数据存入队列是add_queue:

    • 将尾指针后移,tail+1。
    • 若尾指针等于队列的最大下标MaxSize-1,则将数据存入 tail所指的数组元素中,否则无法存入。tail == MaxSize-1,则队列满了。
  • 实现不限长度的队列:
    在这里插入图片描述
    说明:

  1. 上面代码实现了基本队列结构,但是没有有效的利用数组/切片的空间。
  2. 优化:使用数组,实现一个环形的队列。
数组模拟环形队列

参考:尚硅谷-数组模拟循环队列

  • head含该元素,tail也不含该元素。
  • 如果前面是用数组实现的,为充分利用数组,可将数组看作是环形的。【取模%maxSize】
  • 将队列容量空出一个作为约定,队列满的条件:(tail+1)%maxSize == head
  • tail==head 为空。
  • 初始化,head=0,tail=0。
  • 队列中元素的数量:(tail + maxSize - head) % maxSize
    在这里插入图片描述

哈希表

哈希函数

  • 背景问题:把单词放入0-29的槽位,然后能够O(1)直接找到。
  • 通过某种算法把单词换算成0-29数字。
    在这里插入图片描述

哈希碰撞

  • 背景问题:多个单词都会换算到相同的数字
    在这里插入图片描述
解决方法
  • 拉链法:每个槽位都存储为链表。碰撞过多时,查询的时间复杂度退化。
  • 开放寻址法(open addressing):
    • 线性探查法:当槽位被占用,寻找下一个可以使用的槽位。
    • 二次探查法:当槽位被占用,以二次方作为偏移量。Python内置使用该方法。
    • 双重散列法:二次哈希。
哈希函数的优化
  • 装载因子:决定如何开辟新的内存空间
  • 重哈希(Rehash)

List vs Map vs Set

  • List即列表
  • Map,即映射数据结构(KEY与Value),在Python中即为Dict字典
  • Set,集合,不允许有重复的元素,也可理解为只有KEY的Map。

哈希表与二叉树

字典和集合一般是基于哈希表或二叉树实现。

HashMap/HashSetTreeMap/TreeSet
查询O(1)O(logN)
排序无序相对有序
  • Python的Dict:基于HashMap
  • Java有 HashMap与TreeMap

递归(Recursion)

递归的本质是循环,通过函数体进行的循环。

n的阶乘(n!)

  • 问题:n! = 1*2*3*...*n
  • 注意:死循环,必须有递归出口。
  • 代码示例:
    在这里插入图片描述

递归思想剖析

先层层递进,然后层层返回。
在这里插入图片描述

递归模板

  • recursion terminator:递归终止条件
  • level:递归层级
  • process logic in current level:在当前层级,处理具体业务逻辑
  • drill down:进入下层(p1:新参数)
  • reverse:返回当前层级的结果,可选

在这里插入图片描述

斐波那切数列(Fabonacci Array)

  • 斐波那切数列的示例:1, 1, 2, 3, 5, 8, 13, 21, 34, …
  • 公式:F(n-1) + F(n-2)
  • 代码示例:
    在这里插入图片描述
  • 注意:递归的傻瓜实现(千万不能作为面试题答案)
  • 时间复杂度为:O(2**n),数字过大时会超过最大递归层级(栈溢出)

F(6)的递归层级剖析

  • 复杂度是O(2^n),虽然层级近似2^6
  • 有大量的重复子操作,F(3)有三次。

在这里插入图片描述

调用栈

递归是基于 调用栈 实现

调用栈的层级剖析
  • 问题背景:用递归实现多次打印
    在这里插入图片描述
  • 栈区与层级示意图(方法栈的入栈与出栈)
    在这里插入图片描述

斐波那契数列的优化

  • 参考:斐波那契数列的5种python实现写法
  • 递归法:O(2^n),大量重复计算,递归深度为1000。
  • 递推法:O(n),线性增长。
  • 递归优化:缓存中间数,减少重复计算,空间换时间。

在这里插入图片描述

golang实现

在这里插入图片描述

分治(Divide & Conquer)

也是一种递归

  • 基本思想:把大问题分解为多个小问题
    在这里插入图片描述

字符串变大写

  • 问题:把某字符串的所有字符变为大写
    在这里插入图片描述

  • 解决方案

    1. 遍历:每个字符均变为大写
    2. 递归:把第一个字符变大写,剩下放入下一层处理
    3. 分治:把字符串拆开,然后分别变大写,最后拼接在一起。优势:可以并行,提高速度。
  • 仍然存在子问题重复操作的问题

    • 动态规划,可以解决
    • 子问题记忆,可以解决

分治模板

  • problem:大问题,即递归层级
  • subproblem:子问题
  • conquer subproblem:分解为子问题
  • generate the final result:汇总子结果
    在这里插入图片描述

基础排序算法

冒泡、选择和插入算法的复杂度均为O(N^2)

  • 代码:basic_sort.py

冒泡排序

  • 问题背景:10 个小盆友从左到右站成一排,身高不等,老师让其按身高排队。
  • 思路分析:
    • 第一轮:每次左右比较,高个站右边,9次比较后,最高个换到最右侧
    • 第二轮:8次比较后,次高个换到倒数第二个位置
    • 第九轮:1次比较
  • 代码示例
    在这里插入图片描述
  • 排序过程:
# 不断把大元素挤到右侧
第1轮:[46, 39, 12, 73, 33, 99, 6, 51, 53, 38]
第2轮:[39, 12, 46, 33, 73, 6, 51, 53, 38, 99]
第3轮:[12, 39, 33, 46, 6, 51, 53, 38, 73, 99]
第4轮:[12, 33, 39, 6, 46, 51, 38, 53, 73, 99]
第5轮:[12, 33, 6, 39, 46, 38, 51, 53, 73, 99]
第6轮:[12, 6, 33, 39, 38, 46, 51, 53, 73, 99]
第7轮:[6, 12, 33, 38, 39, 46, 51, 53, 73, 99]
第8轮:[6, 12, 33, 38, 39, 46, 51, 53, 73, 99]
第9轮:[6, 12, 33, 38, 39, 46, 51, 53, 73, 99]

golang实现

参考视频:尚硅谷-golang-冒泡排序

在这里插入图片描述

选择排序

  • 问题背景:同样10个身高不等的小朋友,按身高排队
  • 思路分析:
    • 从第一个开始,从头到尾找一个个头最小的小盆友,然后把它和第一个小盆友交换。
    • 然后从第二个小盆友开始采取同样的策略,这样一圈下来小盆友就有序了。
  • 代码示例
    在这里插入图片描述
  • 排序过程:
# 不断找小元素插入左侧
第1轮:[6, 39, 12, 73, 33, 99, 46, 51, 53, 38]
第2轮:[6, 12, 39, 73, 33, 99, 46, 51, 53, 38]
第3轮:[6, 12, 33, 73, 39, 99, 46, 51, 53, 38]
第4轮:[6, 12, 33, 39, 73, 99, 46, 51, 53, 38]
第5轮:[6, 12, 33, 39, 46, 99, 73, 51, 53, 38]
第6轮:[6, 12, 33, 39, 46, 51, 73, 99, 53, 38]
第7轮:[6, 12, 33, 39, 46, 51, 53, 99, 73, 38]
第8轮:[6, 12, 33, 39, 46, 51, 53, 73, 99, 38]
第9轮:[6, 12, 33, 39, 46, 51, 53, 73, 99, 38]

golang实现

参考视频:尚硅谷-golang-选择排序

在这里插入图片描述

插入排序

  • 问题背景:同样10个身高不等的小朋友,按身高排队

  • 思路分析:

    • 第一次,第二个小朋友与第一个进行比较并排序
    • 第二次,第三个小朋友 与 前2个(前2个已有序)比较,并放入合适的位置
    • 第十次抽取并插入新队,即得到有序队列
  • 代码示例:
    在这里插入图片描述

  • 排序过程:

# 不断把新元素放到已经有序的数组中
第1轮:[39, 46, 12, 73, 33, 99, 6, 51, 53, 38]
第2轮:[12, 39, 46, 73, 33, 99, 6, 51, 53, 38]
第3轮:[12, 39, 46, 73, 33, 99, 6, 51, 53, 38]
第4轮:[12, 33, 39, 46, 73, 99, 6, 51, 53, 38]
第5轮:[12, 33, 39, 46, 73, 99, 6, 51, 53, 38]
第6轮:[6, 12, 33, 39, 46, 73, 99, 51, 53, 38]
第7轮:[6, 12, 33, 39, 46, 51, 73, 99, 53, 38]
第8轮:[6, 12, 33, 39, 46, 51, 53, 73, 99, 38]
第9轮:[6, 12, 33, 38, 39, 46, 51, 53, 73, 99]

golang实现

参考视频:尚硅谷-插入排序

在这里插入图片描述

高级排序算法

分治法与归并排序

  • 时间复杂度:O(n logn)(不忽略常数项为:O(cn logn + cn)
  • 代码示例:merge_sort.py
  • 基本思路:归并排序把数组递归成只有单个元素的数组,之后再不断两两 合并,最后得到一个有序数组。

分治法

  • 代码示例:
    在这里插入图片描述

  • 分治过程详解

[46, 39, 12, 73, 33, 99, 6, 51, 53, 38]
                     ↓
[46, 39, 12, 73, 33]   [99, 6, 51, 53, 38]
        ↓                          ↓
[46, 39] [12, 73, 33]       [99, 6] [51, 53, 38]
    ↓          ↓                ↓         ↓
[46] [39]   [12] [73, 33]   [99] [6]  [51] [53, 38]
			         ↓                 		      ↓
                 [73] [33]                    [53] [38]

归并两个有序数组

A = [1, 3, 5, 7, 9, 11]
B = [0, 2, 8, 9, 11, 15, 16, 17]
# 如何得到有序的 new_seq ?
  • 基本思路:两个数组的指针从头开始,相互比较大小,然后逐个右移。

  • 复杂度:O(max(m, n))(m=len(A), n=len(B))

  • 代码示例:
    在这里插入图片描述

  • 归并过程详解


[46] [39]   [12]   [73] [33]   [99] [6]   [51]   [53] [38]
    ↓         ↓        ↓           ↓        ↓        ↓
[39, 46]      ↓     [33, 73]     [6, 99]    ↓     [38, 53]
    ↓              ↓                ↓            ↓
    ↓         [12, 33, 73]          ↓       [38, 51, 53]
        ↓                                ↓
[12, 33, 39, 46, 73]             [6, 38, 51, 53, 99]
                           ↓
[6, 12, 33, 38, 39, 46, 51, 53, 73, 99]

快速排序

  • 代码示例:quick_sort.py

  • 时间复杂度:O(n*logn)

  • 很多程序语言的内置排序都有它的影子。

  • 快排也是一种分而治之(divide and conquer)的策略

  • 快速排序的基本步骤:

    1. 选择基准值 pivot 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。这个过程称之为 partition
    2. 对这两个子数组进行快速排序。
    3. 合并结果
  • 代码示例:简单粗暴地直译快排三大步骤
    在这里插入图片描述

  • 快速排序过程详解

# 分治
[46, 39, 12, 73, 33, 99, 6, 51, 53, 38]
                       ⬇️ 
[39, 12, 33, 6, 38]    46    [73, 99, 51, 53]
             ⬇️                     ⬇️
[12, 33, 6, 38] 39 []       [51, 53] 73 [99]
    ⬇️                         ⬇️
[6] 12 [33, 38]             [] 51 [53]
   ⬇️
[] 33 [38]

# 然后,合并
  • 缺陷:

    • less_part和great_part需要额外的存储空间
    • partition操作每次都要两次遍历整个数组
  • inplace原地排序,来实现parition操作

  • 优化后的partition操作
    在这里插入图片描述

  • 代码示例:优化后的快排(原地排序,partition只遍历一遍数组)
    在这里插入图片描述

golang实现

使用归并,不是指针
  • 效率低
    在这里插入图片描述
使用指针

参考视频:尚硅谷-快速排序

  • 效率高

在这里插入图片描述

基本概念

  • 树状结构是对 链表的进化。

  • 链表(Linked List)
    在这里插入图片描述

  • 树(Tree)

    • 根节点:Root
    • 子树:Sub-tree
    • 父节点:Parent Node
    • 子节点:Child Node
    • 左/右节点:Left/Right Node
    • 兄弟节点:Siblings
    • 层级:Level,树的深度
    • 树的高度:Level+1,因为Level从0开始
    • 树的宽度:包含节点最多的层级的节点数量
    • 树的size:二叉树的节点总个数
      在这里插入图片描述

二叉树

Binary Tree

  • 完全二叉树:每个父节点均有左/右两个子节点。
    在这里插入图片描述

  • 图(Graph):可以指向前节点,甚至任意节点
    在这里插入图片描述

  • 小结:

    • Linked List就是特殊化的Tree
    • Tree就是特殊化的Graph

如何构建树节点

  • Python:
    在这里插入图片描述

  • Java:
    在这里插入图片描述

二叉搜索树

工程中常用二叉搜索树

  • 二叉搜索树(Binary Search Tree),又称二叉查找树、有序二叉树(Ordered Binary Tree)。

  • 排序二叉树(Sorted Binary Tree)是指一棵空树或者具有下列性质的二叉树。

    • 左子树上所有节点的值均小于它的根节点的值
    • 右子树上所有节点的值均大于它的根节点的值
    • Recursively,左、右子树也分别为二叉搜索树
  • 示例图:典型的二叉搜索树
    在这里插入图片描述

  • 查找数据的复杂度为:O(logN)

  • 性能优化:当二叉搜索树的性能退化时,可以打乱并重构为新的二叉搜索树。

  • 进化数据结构:前三种最差情况也为O(logN)

    • 红黑树
    • Splay树
    • AVL树
    • KD树

二叉树的遍历

  • 三种遍历方式:

    • 前序(Pre-order):根-左-右
    • 中序(In-order):左-根-右
    • 后序(Post-order):左-右-根
  • 实际工程使用:

    • 深度优先
    • 广度优先
    • 搜索

遍历顺序

  • 图示:二叉树遍历顺序

    • 前序:A-B-D-E-C-F-G
    • 中序:D-B-E-A-F-C-G
    • 后序:D-E-B-F-G-C-A
      在这里插入图片描述
  • 伪代码实现:基于递归
    在这里插入图片描述

构建二叉树的代码实现

  • 代码:Tree.py

  • 如何构建如图的二叉树
    在这里插入图片描述

  • 构建如下列表
    在这里插入图片描述

  • 构建节点
    在这里插入图片描述

  • 利用NodeList构建二叉树
    在这里插入图片描述

  • 先序遍历:

    • 利用递归
      在这里插入图片描述
  • 二叉树(左右)反转:

    • 与遍历类似
      在这里插入图片描述

二叉查找树(Binary Search Tree, BST)的代码实现

  • 典型的二叉查找树
    • 中(根)序排序:得到从小到大的数组
      在这里插入图片描述

BST构建

  • 构建如上的二叉查找树
    • 与普通二叉树构建一样
      在这里插入图片描述

BST操作

查找
  • key在二叉搜索树中查找

在这里插入图片描述

查找最小/大节点
  • 二叉查找树的最左端就是最小节点
    在这里插入图片描述

  • 最大就是往最右端找

插入
  • 插入节点时,我们需要保持 BST 的特性,每次插入一个节点,我们都通过递归比较把它放到正确的位置。

  • 你会发现新节点总是被作为叶子结点插入。(请你思考这是为什么?)

  • 插入示例:
    在这里插入图片描述

  • 代码实现
    在这里插入图片描述

删除
  • 删除节点有三种情况:

    • 节点是叶节点(无子节点)
    • 节点有一个孩子(有一个子节点)
    • 节点有两个孩子(有两个子节点)
  • 无子节点:这是最简单的一种情况,只需要把它的父节点指向它的指针设置为 None 就好。
    在这里插入图片描述

  • 有一个子节点:删除有一个孩子的节点时,我们拿掉需要删除的节点,之后把它的父亲指向它的孩子就行,因为根据 BST 左子树都小于节点,右子树都大于节点的特性,删除它之后这个条件依旧满足。
    在这里插入图片描述

  • 有两个子节点:

    • 下图方式会破坏二叉查找树的性质
      在这里插入图片描述

    • 如果中序遍历 BST 并且输出每个节点的 key,你会发现就是一个有序的数组。 [1 4 12 23 29 37 41 60 71 84 90 100]。这里定义两个概念,逻辑前任(predecessor)和后继(successor),请看下图:

      • 逻辑后继,即为该节点右子树的最左端节点。
        在这里插入图片描述
    • 步骤:

      1. 找到待删除节点 N(12) 的后继节点 S(23)
      2. 复制节点 S 到节点 N
      3. 从 N 的右子树中删除节点 S,并更新其删除后继节点后的右子树
  • 代码实现:略

堆(heap)

  • 时间复杂度为O(nlogn)

堆的概念介绍

  • 堆是一种完全二叉树,有最大堆和最小堆两种。

  • 最大堆: 对于每个非叶子节点 V,V 的值都比它的两个孩子大,称为 最大堆特性(heap order property) 最大堆里的根总是存储最大值,最小的值存储在叶节点。
    在这里插入图片描述

  • 最小堆:和最大堆相反,每个非叶子节点 V,V 的两个孩子的值都比它大。
    在这里插入图片描述

堆的表示

  • 基于 数组 实现堆。

在这里插入图片描述

  • 对于数组里的一个下标 i,我们可以得到它的父亲和孩子的节点对应的下标:
parent = int((i-1) / 2)    # 取整
left = 2 * i + 1
right = 2 * i + 2
  • 超出下标表示没有对应的孩子节点。

堆的操作

  • 基于array_adt.py实现

插入新节点

  • 插入新的值:为了维持堆的特性,需要sift-up操作
  • sift-up函数:通过递归,确保新节点小于其父节点,如果大于其父节点则相互交换 并继续寻找,直到根节点。
    在这里插入图片描述

删除根节点

  • 获取并移除根节点,即删除最大堆的最大值。需要sift-down操作
  • sift-down函数:把根节点值移除,把最后一个节点复制到根节点位置,逐层(递归)比较该节点与孩子节点的值,如果孩子值更大 则 该节点与孩子节点 互换,直到满足该节点比孩子大,即满足最大堆特性。
    在这里插入图片描述

堆排序

  • 基于最大堆,实现降序排序:
    • 最大堆的每次extract都得到最大值
      在这里插入图片描述

二分查找

线性查找

  • 线性查找:从头找到尾,直到符合条件了就返回。
number_list = [0, 1, 2, 3, 4, 5, 6, 7]

def linear_search(value, iterable):
    for index, val in enumerate(iterable):
        if val == value:
            return index
    return -1

什么是二分查找(Binary Search)

  • 前提条件:

    • 只能在有序(递增/递减)的数组中实现
    • 数组必须存在上下界
    • 能通过索引访问
  • 时间复杂度:O(logN)

  • 缺点:必须是有序数组

代码模板

  • 假设:单调递增数组

    • 左部分、分界点 和 右部分
    • 与分界点比较,确定往左 或 往右 继续找
      在这里插入图片描述
  • 具体代码
    在这里插入图片描述

二分查找详细过程

  • 在该递增数组中,寻找31
  • 前两次查找
    在这里插入图片描述
  • 第三次查找
    在这里插入图片描述

相关模块

  • bisect模块
  • itertools模块和常见的几个函数(takewhile, dropwhile, from_iterable, count, tee)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值