数据结构与算法是计算机科学中至关重要的概念,它们相互依存、相辅相成。下面是一些基础知识:
数据结构(Data Structures):
-
数组(Array):一组连续存储的相同类型数据的集合,通过索引访问元素。
-
链表(Linked List):由节点组成的线性数据结构,每个节点包含数据和指向下一个节点的引用。
-
栈(Stack):后进先出(LIFO)的数据结构,只允许在栈顶进行插入和删除操作。
-
队列(Queue):先进先出(FIFO)的数据结构,允许在队尾插入元素,在队头删除元素。
-
树(Tree):由节点组成的层次结构,每个节点最多有一个父节点和多个子节点。
-
图(Graph):由节点(顶点)和连接这些节点的边组成的非线性数据结构。
算法(Algorithms):
-
搜索算法:用于在数据集中查找特定项的算法,如线性搜索、二分搜索等。
-
排序算法:用于将数据按照特定顺序排列的算法,如冒泡排序、快速排序、归并排序等。
-
递归算法:通过将问题分解为相似的子问题来解决问题的算法,如斐波那契数列的递归解法。
-
动态规划:将原问题分解为相对简单的子问题,并将子问题的解保存起来以避免重复计算的算法。
-
贪心算法:每一步都采取当前状态下最好或最优选择的算法,通常用于组合优化问题。
-
回溯算法:通过尝试所有可能的候选解来解决问题的算法,常用于组合问题和排列问题。
知识点补充:
-
复杂度分析:对算法的时间复杂度和空间复杂度进行评估,了解算法的运行效率。
-
数据结构与算法的选择:根据问题的特性选择合适的数据结构和算法来解决问题,考虑时间复杂度和空间复杂度等因素。
-
算法设计思想:例如分治、贪心、动态规划等,了解不同的算法设计思想有助于解决各种复杂的问题。
-
实践与应用:通过练习和实际应用,深入理解数据结构与算法,并提升解决实际问题的能力。
1数组理论基础
数组是存放在连续内存空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
举一个字符数组的例子,如图所示:
内存地址 | 100 | 101 | 102 | 103 | 104 | 106 | 107 | 108 |
字符数组 | a | b | c | d | a | j | i | j |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
需要两点注意的是
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。数组的元素是不能删的,只能覆盖。
那么二维数组在内存的空间地址是连续的么?
不同编程语言的内存管理是不一样的
在C语言中,二维数组被存储为一块连续的内存区域,其中每个元素的存储位置是连续的,这也适用于多维数组。
在C++中,二维数组可以使用数组指针或者动态分配内存来表示。如果使用数组指针,二维数组在内存中是连续存储的,类似于C语言中的方式。如果使用动态分配内存,二维数组的存储空间也是连续的,因为动态分配的内存是连续的。
在Python中,二维数组通常是通过列表的列表来表示的。每个内部列表代表二维数组的一行,而整个列表则代表二维数组本身。这种方式下,内存中的存储并不是连续的,因为每个内部列表在内存中可能存储在不同的位置。
2链表理论基础
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
链表的类型:单链表(如上图所示)
双链表
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。如图所示:
循环链表
循环链表,顾名思义,就是链表首尾相连。如图所示:
链表的存储方式
了解完链表的类型,再来说一说链表在内存中的存储方式。
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。如图所示:
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
性能分析
再把链表的特性和数组的特性进行一个对比,如图所示:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
3哈希表理论基础
首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。
其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
-
数据结构
底层实现 优点 缺点 C++ Set 红黑树 快速查找、插入和删除操作;有序性好 占用内存较大 C++ Map 红黑树 快速查找、插入和删除操作;有序性好 占用内存较大 C Set 哈希表 快速查找操作;占用内存相对较小 无序性;插入和删除操作可能较慢 Python Set 哈希表 快速查找操作;占用内存相对较小 无序性;不支持索引访问 Python Dictionary (Map) 哈希表 快速查找操作;占用内存相对较小 无序性;不支持索引访问 C++ Set 和 Map:
底层实现:使用红黑树实现,保持元素有序。 优点:快速查找、插入和删除操作;有序性好。缺点:占用内存较大C Set:
- 底层实现:使用哈希表实现。
- 优点:快速查找操作;占用内存相对较小。
- 缺点:无序性;插入和删除操作可能较慢。
Python Set:
- 底层实现:使用哈希表实现。
- 优点:快速查找操作;占用内存相对较小。
- 缺点:无序性;不支持索引访问。
Python Dictionary (Map):
- 底层实现:使用哈希表实现。
- 优点:快速查找操作;占用内存相对较小。
- 缺点:无序性;不支持索引访问。
在C++, C语言 ,python中,set 和 map 分别提供的数据结构 其底层实现以及优劣如上表所示。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
-
4栈与队列理论基础
- 队列是先进先出,栈是先进后出。如图所示:
栈(Stack):
-
定义:栈是一种具有后进先出(Last In, First Out,LIFO)特性的线性数据结构。在栈中,最后插入的元素是最先被移除的。
-
操作:栈通常支持以下操作:
- Push:将元素压入栈顶。
- Pop:从栈顶移除元素。
- Peek(或 Top):查看栈顶元素,但不移除。
- isEmpty:检查栈是否为空。
-
应用:栈常用于需要后进先出顺序的场景,例如函数调用、表达式求值、浏览器的前进后退功能等。
队列(Queue):
-
定义:队列是一种具有先进先出(First In, First Out,FIFO)特性的线性数据结构。在队列中,最先插入的元素是最先被移除的。
-
操作:队列通常支持以下操作:
- Enqueue:将元素插入队列尾部。
- Dequeue:从队列头部移除元素。
- Front:查看队列头部元素,但不移除。
- isEmpty:检查队列是否为空。
-
应用:队列常用于需要先进先出顺序的场景,例如任务调度、消息传递、缓冲区管理等。
理论基础:
-
数据结构:栈和队列都是基于线性数据结构设计的,它们的基本操作都是在线性结构上进行的。
-
抽象数据类型(ADT):栈和队列是抽象数据类型的实现之一,它们定义了一组操作,但并未指定具体的实现方式。
-
实现方式:栈和队列可以通过不同的数据结构来实现,例如数组、链表等。在实际应用中,选择合适的实现方式可以提高操作的效率。
-
应用场景:栈和队列在计算机科学中有广泛的应用,理解它们的特性和操作可以帮助解决各种问题,优化算法和数据结构的设计。
5二叉树理论基础知识
二叉树(Binary Tree):
-
定义:二叉树是一种特殊的树形数据结构,其中每个节点最多有两个子节点,称为左子节点和右子节点。这些子节点必须符合特定的顺序性,通常是左子节点小于父节点,右子节点大于父节点,但也可以是其他顺序。
-
节点:二叉树的节点通常包含一个数据元素和指向左右子节点的指针(或引用)。
-
特性:
- 每个节点最多有两个子节点。
- 左子节点小于父节点,右子节点大于父节点(BST)。
- 可以有空子节点。
-
类型:
- 二叉搜索树(Binary Search Tree,BST):一种特殊的二叉树,满足左子节点小于父节点,右子节点大于父节点的顺序性。这种特性使得BST在查找、插入和删除等操作上有较高的效率。
- 平衡二叉树(Balanced Binary Tree):一种高效的二叉树,保持左右子树的高度差不超过一个固定常数,以确保在最坏情况下的操作复杂度不会偏离对数级别。
- 完全二叉树(Complete Binary Tree):除了最后一层外,其他每一层的节点都被完全填充,并且最后一层从左到右依次填充。这种树在存储和操作上有一些特殊的性质。
-
操作:
- 遍历:包括前序遍历(中左右)、中序遍历(左中右)、后序遍历(左右中)和层序遍历等方法,用于访问树中的所有节点。
- 插入和删除:在二叉搜索树中,插入和删除操作需要维护树的顺序性,并可能导致树的结构调整,以保持二叉搜索树的性质。
-
应用:
- 二叉搜索树常用于实现关联数组、集合和映射等数据结构。
- 平衡二叉树常用于数据库索引和高性能数据存储结构的设计。
- 完全二叉树常用于堆数据结构的实现,用于优先队列和排序算法等。
理解二叉树的理论基础对于设计高效的算法和数据结构以及解决各种问题具有重要意义。
6图理论基础知识
图(Graph):
图是由节点(顶点)和连接这些节点的边组成的非线性数据结构。图可用于表示各种实际问题中的关系和网络。
-
节点(顶点):图中的基本单元,可以表示各种实体或对象。
-
边:连接节点的线段,用于表示节点之间的关系或连接。
-
有向图(Directed Graph):边有方向的图,即边有箭头指向,表示从一个节点到另一个节点的方向。
-
无向图(Undirected Graph):边没有方向的图,表示节点之间的关系是双向的。
-
权重(Weight):边上的数值,表示连接两个节点的成本、距离或其他度量。
图的表示方法:
-
邻接矩阵:使用二维数组表示图中节点之间的连接关系,适用于稠密图。
-
邻接表:使用数组和链表结合的方式表示图中节点之间的连接关系,适用于稀疏图。
图的遍历算法:
-
深度优先搜索(Depth-First Search,DFS):从起始节点开始,沿着一条路径尽可能深地访问节点,直到不能继续为止,然后回溯并探索未访问的分支。
-
广度优先搜索(Breadth-First Search,BFS):从起始节点开始,先访问其所有邻居节点,然后依次访问邻居节点的邻居节点,以此类推,直到所有节点都被访问。
图的应用:
-
社交网络分析:表示人与人之间的关系网络。
-
网络路由:表示网络中的路由节点和连接关系。
-
地图导航:表示城市之间的道路连接关系,用于导航和路径规划。
-
组织结构:表示企业或组织中的部门、员工之间的关系。
-
编译器设计:表示程序中的控制流和数据流,用于编译器优化和代码生成。
7回溯算法
-
定义:回溯算法是一种通过尝试所有可能的候选解来解决问题的算法。在搜索过程中,当发现当前路径不能满足问题的要求时,会回溯到上一步,尝试其他的路径,直到找到解或者确定不存在解为止。
-
特性:
- 回溯算法通常通过递归的方式实现。
- 在搜索的过程中,会维护一个候选解的集合,并逐步扩展和修改这个集合,直到满足问题的条件或者搜索空间被完全遍历。
- 回溯算法通常用于解决组合问题、排列问题、子集问题等需要遍历所有可能情况的场景。
-
步骤:
- 选择:根据问题的要求,从当前的候选解集合中选择一个元素作为下一步的尝试。
- 路径:将选择的元素添加到当前路径中,并进入下一层递归。
- 条件:判断当前路径是否满足问题的条件,如果满足则将当前路径作为解,否则回溯到上一步。
- 回溯:在回溯过程中,撤销当前路径的选择,尝试其他的选择,直到找到解或者确定不存在解。
-
应用:
- 组合问题:如从给定集合中选取若干元素组成组合。
- 排列问题:如给定一组元素,求其所有可能的排列方式。
- 子集问题:如给定一组元素,求其所有可能的子集。
-
优化:
- 剪枝:在搜索过程中,通过一些条件判断可以提前终止无效的搜索分支,从而减少搜索空间,提高算法效率。
- 记忆化搜索:在搜索过程中记录已经访问过的状态或路径,避免重复计算,提高搜索效率。
回溯算法是一种强大的搜索算法,但在处理复杂问题时可能会面临指数级别的时间复杂度,因此需要结合剪枝等优化技巧来提高效率。
8贪心算法
-
定义:回溯算法是一种通过尝试所有可能的候选解来解决问题的算法。在搜索过程中,当发现当前路径不能满足问题的要求时,会回溯到上一步,尝试其他的路径,直到找到解或者确定不存在解为止。
-
特性:
- 回溯算法通常通过递归的方式实现。
- 在搜索的过程中,会维护一个候选解的集合,并逐步扩展和修改这个集合,直到满足问题的条件或者搜索空间被完全遍历。
- 回溯算法通常用于解决组合问题、排列问题、子集问题等需要遍历所有可能情况的场景。
-
步骤:
- 选择:根据问题的要求,从当前的候选解集合中选择一个元素作为下一步的尝试。
- 路径:将选择的元素添加到当前路径中,并进入下一层递归。
- 条件:判断当前路径是否满足问题的条件,如果满足则将当前路径作为解,否则回溯到上一步。
- 回溯:在回溯过程中,撤销当前路径的选择,尝试其他的选择,直到找到解或者确定不存在解。
-
应用:
- 组合问题:如从给定集合中选取若干元素组成组合。
- 排列问题:如给定一组元素,求其所有可能的排列方式。
- 子集问题:如给定一组元素,求其所有可能的子集。
-
优化:
- 剪枝:在搜索过程中,通过一些条件判断可以提前终止无效的搜索分支,从而减少搜索空间,提高算法效率。
- 记忆化搜索:在搜索过程中记录已经访问过的状态或路径,避免重复计算,提高搜索效率。
回溯算法是一种强大的搜索算法,但在处理复杂问题时可能会面临指数级别的时间复杂度,因此需要结合剪枝等优化技巧来提高效率。
9动态规划
-
定义:动态规划是一种通过将原问题分解为相对简单的子问题的方式来求解复杂问题的方法。与分治算法类似,但动态规划通常用于解决重叠子问题和具有最优子结构性质的问题。
-
特性:
- 最优子结构:原问题的最优解可以通过子问题的最优解来求解。
- 重叠子问题:在求解过程中,会重复计算相同的子问题。
-
步骤:
- 划分子问题:将原问题划分为相对简单的子问题。
- 解决子问题:递归地或迭代地求解子问题。
- 合并子问题:将子问题的解合并起来,得到原问题的解。
-
应用:
- 最优化问题:如最长递增子序列、最大子数组和、最短路径等。
- 计数问题:如组合问题、排列问题等。
- 决策问题:如背包问题、装配线调度问题等。
-
步骤:
- 定义状态:明确定义问题的状态,将问题转化为状态之间的转移。
- 状态转移方程:根据问题的特性,建立状态之间的转移关系。
- 初始化:对边界状态进行初始化。
- 状态转移:按照状态转移方程递推或者迭代地求解问题。
- 解的表示:根据问题的要求,确定最终状态的表示方式,得到最优解或最优值。
-
优化:
- 记忆化搜索:将子问题的解保存起来,避免重复计算。
- 状态压缩:对状态空间进行压缩,减少空间复杂度。
- 剪枝:在搜索过程中排除一些无效的状态,提高效率。
-
例子:
- Fibonacci 数列:通过动态规划可以将其时间复杂度从指数级别降低到线性级别。
- 背包问题:利用动态规划可以有效求解背包问题,包括0-1背包问题和分数背包问题等。
动态规划是一种强大的算法设计思想,可以解决各种复杂的优化、计数和决策问题,但需要注意状态的定义、状态转移方程的建立以及边界情况的处理。