@TOC
数据结构(data structure)是计算机中存储、组织数据的方式。
数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合。它包含三方面的内容,逻辑关系、存储关系及操作。
Ⅰ.1 栈,先进后出(Stack,Last in First Out)
一种特殊的线性表,只能在一个表的一个固定端进行数据节点的插入和删除操作。
Ⅰ.2 队列,先入先出(Queue,First in First Out)
一种特殊的线性表。只允许在表的一端进行插入操作,而在另一端进行删除操作。
Ⅰ.3 数组(Array)
一种线性表数据结构,将具有相同类型的若干变量有序地组织在一起的集合。
Ⅰ.4 散列表(Hash table)
源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。
散列表又叫哈希表,存储的是由键(key)和值(value)组成的数据,根据键直接访问存储在内存存储位置的数据结构。
哈希表的底层是基于数组来存储的,
● 当插入键值对时,并非直接插入该数组中,而是通过对键进行Hash运算得到Hash值,然后和数组容量取模,得到在数组中的位置后再插入。
● 取值时,先对指定的键求Hash值,再和容量取模得到底层数组中对应的位置,如果指定的键值与存贮的键相匹配,则返回该键值对,如果不匹配,则表示哈希表中没有对应的键值对。
Ⅰ.5 链表(Linked List)
一种数据元素按照链式存储结构进行存储的数据结构,由一系列节点组成(所谓节点是指链表中的元素)。每个节点包含两个数据,一个是存储元素的数据域(值),另一个是存储下一个节点地址的指针域。这种存储结构具有在物理上存在非连续的特点。
Ⅰ.6 树(Tree)
一种层级式的数据结构,由顶点(节点)和连接它们的边组成。树的结构特点是:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树。
| 类别 | 定义 |
|---|---|
| 二叉搜索树(Binary Search Tree) | 又称为二叉排序树,若二叉树的左子树不空,则左子树上的所有结点的值均小于它的根节点的值;若二叉树的右子树不空,则右子树上的所有结点的值均大于它的根节点的值;并且该二叉树的左右子树又分别为二叉排序树。 |
| 平衡二叉查找树(Balanced Binary Tree) | 又称为平衡二叉树、AVL树,是基于二叉查找树优化而来的,若树上任一结点的左子树和右子树的深度之差不超过 1 则为平衡二叉树。 |
| 真二叉树(Proper Binary Tree) | 所有节点的度都要么是0,要么是2 |
| 满二叉树(Full Binary Tree) | 除了叶子节点之外的每个节点都有左右两个子节点,且所有的叶子节点都在最后一层 |
| 完全二叉树(Complete Binary Tree) | 叶子节点只会出现最后两层,且最后一层的叶子节点都靠左对齐 |
| 堆(Heap) | 一种特殊的完全二叉树,堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。 |
| 红黑树(Red Black Tree) | 1)每个节点要么是红的,要么是黑的;2)根节点是黑的;3)每个叶节点,即空节点(NIL)是黑的;4)如果一个节点是红的,那么它的两个儿子都是黑的;5)对每个节点,从该节点到其子孙节点的所有路径上包含相同数目的黑结点。 |
二叉树的遍历方式:
● 前序遍历 (LeetCode 144):按照 根节点 -> 左孩子 -> 右孩子 的方式遍历,即「先序遍历」,每次先遍历根节点;
● 中序遍历 (LeetCode 94):按照 左孩子 -> 根节点 -> 右孩子 的方式遍历,即「中序遍历」
● 后序遍历 (LeetCode 145):按照 左孩子 -> 右孩子 -> 根节点 的方式遍历,即「后序序遍历」
● 层序遍历 (LeetCode 102):按照 每一层从左向右 的方式进行遍历。
Ⅰ.7 图(Graph)
一种非线性数据结构,由顶点和连接每对顶点的边所构成。
Ⅰ.1 C++ 数据类型和标准模板库容器(Standard Template Library Containers, STL)
见文章 《C++数据类型及标准库容器(Standard Template Library Containers, STL)》
Ⅰ.2 Python 数据类型
见文章 《Python数据类型》
常用运算:
- 检索: 检索就是在数据结构里查找满足一定条件的节点。一般是给定一个某字段的值,找具有该字段值的节点。
- 插入: 往数据结构中增加新的节点。
- 删除: 把指定的结点从数据结构中去掉。
- 更新: 改变指定节点的一个或多个字段的值。
- 排序: 把节点按某种指定的顺序重新排列。例如递增或递减。
Ⅱ.1 排序
| 排序算法名称 | 时间复杂度 | 排序原理 | 概述思路 |
|---|---|---|---|
| 冒泡排序(Bubble Sort) | O(n^2) | 通过不断地交换“大数”的位置达到排序的目的 | 比较相邻两个数字的大小。将两数中比较大的那个数交换到靠后的位置。不断地交换下去就可以将最大的那个数放到队列的尾部。然后重头再次交换,直到将数列排成有序数列。 |
| 选择排序(Selection Sort) | O(n^2) | 不断将数列中的最小值交换到数列头部实现排序 | 从数列中选择最大(最小)的那个数,将这个数放到合适的位置,然后在删除这个数的子数列中选择最大(最小)的那个数,将这个数放到合适的位置……直到子数列为空。 |
| 插入排序(Insertion Sort) | O(n^2) | 首先将数列分成两部分。数列的第一个数为left部分,其他的数为right部分。然后将right部分中的数逐一取出,插入left部分中合适的位置。当right部分为空时,left部分就成为了一个有序数列 | ① 将数列分成两部分,数列的第一个数为left部分,其他的数为right部分② 将right部分中的数逐一取出,插入left部分中合适的位置 |
| 归并排序(Merge Sort) | O(nLogn) | 将数列不断分成两组直到不能再分,在合并时排序 | 先将数列分成左右两份(最好是等分),然后将左、右子数列排序完毕后再合并到一起就成了一个有序数列;左、右两个子数列变成有序数列的过程是一个递归过程:再把子数列分成左、右两份,把子子数列排序完毕后合并成子数列… |
| 快速排序(Insertion Sort) | O(n^2) | 不断将数列中的最小值交换到数列头部实现排序 | 先以列表中的任意一个数为基准(一般选头或尾),将列表分为左、右两个子列表:左子列表的数要比基准数小,右子列表的数要比基准数大。然后继续把左子列表和右子列表按同样的方法继续分解、比较,直到分无可分。最后将左子列表(比基准数小)+基准数+右子列表(比基准数大)连接起来得到一个有序数列。 |
| 计数排序(Counting Sort) | O(n+k) | 计数排序使用一个额外的数组C ,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C 来将A中的元素排到正确的位置 | ① 找出待排序的数组中最大和最小的元素; ② 统计数组中每个值为i的元素出现次数,存入数组C的第i项; ③ 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加); ④反向填充目标数组:将每个元素ii放在新数组的第C[i]项,每放一个元素就将C[i]减去1 |
| 桶排序(Bucket Sort) | 平均时间复杂度为 O(n+n^2/k+k)(将数据平均分成N块 + 排序 + 重新合并元素),当K≈n时为O(n)。 桶排序的最坏时间复杂度取决于桶内排序的算法 | 将要排序的数据分到有限个有序的桶中,每个桶里的数据再单独进行排序。桶内排序之后,再把每个桶里的数据按照桶的顺序依次取出,得到有序数列。 | ① 设置一个定量的数组当作空桶;② 遍历序列,并将元素一个个放到对应的桶中;③ 对每个不是空的桶进行排序;④ 从不是空的桶里把元素再放回原来的序列中。 |
| 堆排序(Heap Sort) | 建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是O(nlogn) | 构建最大二叉堆,不断从堆顶取出元素,完成排序 | 首先利用待排序数列构建最大堆(根节点保存最大值,父节点数据>子节点数据), 接下来取出堆结构中的根节点元素,即为最大值,保存到新列表的末尾。 更新剩下的数据,组成新的最大堆,并取出根节点元素(最大值),如此不断重复直到堆中的所有数据都被取出,排序完成 |
Ⅱ.2 查找、搜索
| 算法名称 | 复杂度 | 原理 | 实现概述 |
|---|---|---|---|
| 顺序查找(也被称为线性查找) | 时间复杂度O(n),空间复杂度O(1) | 逐一遍历,挨个比较 | 将数列从头到尾按照顺序查找一遍 |
| 二分查找(Binary Search) | 时间复杂度O(logn) | 如果数据是无序的,先要将数据从小到大排序。其原理是比较待查数据与数组中值的数据的大小,如果等于中间值则直接返回,如果大于中间值,就只在后半部分查找,如果小于中间值,就在前半部分查找,如此往复,直到找到数据,或剩下一个元素。 | ①在n个元素中寻找 → ②在n/2个元素中寻找 → ③在n/4个元素中寻找......在1个元素中寻找 |
| 插值查找(Interpolation Search) | 时间复杂度O(loglogn) | 是对二分查找的改进,其应用场景是排序数组中的值是均匀分布的。二分查找总是到中间元素做左右划分,而插值搜索会根据正在搜索的Key的大小来确定划分的位置。 | Mid =L+ (R-L) X (target-data[L])/data[R] - data[L] |
| 分块查找 | - | 先按照一定的取值范围将数列分成数块。块内的元素是可以无序的,但块必须是有序的。(所谓块有序,就是处于后面位置的块中的最小元素都要比前面位置块中的最大元素大) | ①确定待查记录所在块(顺序或折半查找);②在块内查找(顺序查找) |
此外,针对数组和字符串而言:
- 查找有无:某个元素是否存在?通过集合(set)可以解决这一类问题
- 查找对应关系:某个元素出现了几次?返回满足条件元素的下标?通过字典(Dict)可以解决这一类问题
Ⅱ.3 字符串
字符串相关
Ⅱ.3.1 反转字符串(LeetCode 344)
方法一: 双指针
时间复杂度:O(n),其中 N 为字符数组的长度。一共执行了 N/2 次的交换;
空间复杂度:O(1) 。解题思路: ● 将Ieft指向字符数组首元素,right指向字符数组尾元素。 ● 当left < right: 交换s[left]和s[right]; left指针右移一位,即 Ieft=Ieft+1; right指针左移一位,即 right=right-1 ● 当left >= right,反转结束,返回字符数组即可。
方法二:单指针
时间复杂度:O(n),其中 N 为字符数组的长度。一共执行了 N/2 次的交换。
空间复杂度:O(1) 。解题思路: ● 将 指针指向最左侧, ● 当 left < 字符串长度/2: 交换 s[left] 和 s[-left-1] (相当于第一个跟倒数第一交换, 第二跟与倒数第二交换)。 ● 当 left >= 字符串长度/2,反转结束,返回字符数组即可。
Ⅱ.3.2 验证回文串(LeetCode 125)
解法1:
解题思路:
对字符串进行遍历, 只保留数字和字母, 将结果保存到新的字符串s_中。 此时只需判断新的字符串s_ 与其逆序是否相同如果相同则为回文串。
解法2:
时间复杂度:O(|s|),其中 |s| 是字符串 s 的长度。
空间复杂度:O(|s|)。在最坏情况下,新的字符串 s_ 与原字符串 s完全相同,因此需要使用O(|s|) 的空间。解题思路:
对字符串进行遍历, 只保留数字和字母, 将结果保存到新的字符串s_中。 将新字符串是否为回文的判断改用双指针实现
Ⅱ.3.3 无重复字符的最长子串(LeetCode 3)
解法1: 暴力解法
解题思路:
①找到所有的子串 O(n 2) ②确定子串是否有重复字符O(n
解法2: 滑动窗口
时间复杂度:O(n)解题思路:
创建 Set 用来记录出现的字符, 在字符串长度范围以内, 窗口内没有出现重复字符, 窗口右侧向右滑动窗口, 并用Set记录窗口内增加的字符; 遇到重复字符, 窗口停止向右扩大, 窗口左侧向右滑动缩小窗口, 并从set中移除滑出窗口的字符; 在字符串长度范围以内, 窗口内没有出现重复字符, 窗口右侧向右滑动扩大窗口, 并用Set记录窗口内增加的字符;
Ⅱ.3.4 字符串中的第一个唯一字符(LeetCode 387)
方法一:使用哈希表存储频数
时间复杂度:O(n),其中 n 是字符串 s 的长度。我们需要进行两次遍历。
空间复杂度:O(∣Σ∣),其中 Σ 是字符集,在本题中 s 只包含小写字母,因此∣Σ∣≤26。我们需要O(∣Σ∣) 的空间存储哈希映射。思路及算法 对字符串进行两次遍历: 在第一次遍历时,我们使用哈希映射统计出字符串中每个字符出现的次数; 在第二次遍历时,我们只要遍历到了一个只出现一次的字符,那么就返回它的索引,否则在遍历结束后返回-1。
方法二:使用哈希表存储索引
思路及算法 创建一个字典(哈希表),键表示一个字符,值表示它的首次出现的索引(如果该字符只出现一次)或者-1(如果该字符出现多次)。 第一次遍历字符串时,设当前遍历到的字符为 c,如果 c 不在哈希映射中,我们就将c 与它的索引作为一个键值对加入哈希映射中,否则我们将 c 在哈希映射中对应的值修改为 -1。 在第一次遍历结束后,只需要再遍历一次哈希映射中的所有值,找出其中不为 -1 的最小值,即为第一个不重复字符的索引。如果哈希映射中的所有值均为 -1,我们就返回 -1。
Ⅱ.3.5 有效的字母异位词(LeetCode 242)
方法一: 排序
● 时间复杂度:O(nlogn),其中 n 为 s 的长度。排序的时间复杂度为 O(nlogn),比较两个字符串是否相等时间复杂度为O(n),因此总体时间复杂度为 O(nlogn+n)=O(nlogn)。
● 空间复杂度:O(logn)。排序需要 O(logn) 的空间复杂度。解题思路: t 是 s的异位词等价于「两个字符串排序后相等」。 因此我们可以对字符串 s 和 t 分别排序,看排序后的字符串是否相等即可判断。 此外,如果 s 和 t 的长度不同,t 必然不是 s 的异位词。
方法二:哈希表
解题思路: t 是 s 的异位词等价于「两个字符串中字符出现的种类和次数均相等」。 由于字符串只包含 26 个小写字母,因此我们可以通过一个长度为 26 的频次数组 tableS,记录字符串 s 中字符出现的频次; 然后遍历字符串 t,减去 tableS 中对应的频次; 如果出现 tableS[i] != 0,则说明 t 包含一个不在 s 中的额外字符,返回false即可。
Ⅱ.3.6 字母异位词分组(LeetCode 49)
解题思路:当两个字符串互为字母异位词时,两个字符串包含的字母相同。
可使用这一组相同的字母作为一组字母异位词的标志(dict 的 key),使用dict存储每一组字母异位词
解法1: 对每个字符串排序, 排序之后相同的为字母异位词, 将排序之后结果作为dict的 key
● 时间复杂度:O(nklogk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要 O(klogk) 的时间进行排序以及O(1)的时间更新哈希表,因此总时间复杂度是 O(nklogk)。
● 空间复杂度:O(nk)
解法2: 将每个字母出现的次数作为哈希表(dict) 的key
● 时间复杂度:O(n(k+∣ Σ∣ )),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度, Σ 是字符集,在本题中字符集为所有小写字母,∣ Σ∣ =26。需要遍历 n个字符串,对于每个字符串,需要 O(k)的时间计算每个字母出现的次数,O(∣ Σ∣ ) 的时间生成哈希表的键,以及O(1)的时间更新哈希表,因此总时间复杂度是 O(n(k+∣ Σ∣ ))。
● 空间复杂度 O(n(k+∣ Σ∣ ))
Ⅱ.3.7 同构字符串 (LeetCode205)
解题思路:使用字典构建查找表
时间复杂度:O(n)
Ⅱ.4 数组
数组相关
Ⅱ.4.1 查找 - 两数之和 (LeetCode 1)
解法一:暴力解法 双重for循环O(n^2)
解法二:使用查找表(dict),可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到O(1)。
● 时间复杂度:O(N),其中 N是数组中的元素数量。Python dict 查找时间复杂度O(1)
● 空间复杂度:O(N)解题思路: 创建一个字典, 对于每一个 x,首先查询字典中是否存在 target - x, 然后将 x 插入到字典中,即可保证不会让x和自己匹配。
Ⅱ.4.2 查找 - 四数相加 II (LeetCode 454)
解法一:暴力解法 4层循环
时间复杂度:O(n^4) 200^4 = 1,600,000,000
解法二:依据上一个题目(LeetCode 1)的思路 循环三次 将第四个数组放到dict中
时间复杂度:O(n^3) 200^3 = 8,000,000
解法三:按照上面的优化思路继续思考, 我们两两分组, 创建两个dict
时间复杂度:O(n^2) 200^2 = 40,000
空间复杂度:O(n^2)解题思路: 将四个数组分成两部分,A 和 B 为一组,C 和 D 为另外一组。 对于 A 和 B,我们使用二重循环对它们进行遍历,得到所有 A[i]+B[j] 的值并存入哈希映射中。 对于哈希映射中的每个键值对,每个键表示一种 A[i]+B[j],对应的值为 A[i]+B[j]出现的次数。 对于 C 和 D,我们同样使用二重循环对它们进行遍历。 当遍历到 C[k]+D[l] 时,如果 -(C[k]+D[l]) 出现在哈希映射中,那么将-(C[k]+D[l]) 对应的值累加进答案中。 最终即可得到满足 A[i]+B[j]+C[k]+D[l]=0=的四元组数目。
Ⅱ.4.3 双指针 - 移动零 (LeetCode283)
解题思路:从左向右遍历数组,当遇到非零元素时将其复制到新的数组中,遍历结束后,新的数组剩余元素用0补齐:
解法1:
使用两个指针 i , j ,从左向右遍历数组,
j 用来记录非零元素的位置,当 i 遇到非零元素时,将 i 指向的值赋值给 j 指向的元素。
i 遍历结束后,从 j 的位置开始用 0 补齐
解法2:
使用两个指针 i , j ,从左向右遍历数组,
j 用来记录非零元素的位置,当 i 遇到非零元素时,交换 i , j 指向的元素。
Ⅱ.4.4 双指针 - 移除元素 (LeetCode27)
解题思路:删除数组中等于 val 的元素,输出数组的长度一定小于等于输入数组的长度,可以把输出的数组直接写在输入数组上。
● 时间复杂度:O(n),其中n是数组nums的长度
● 空间复杂度:O(1) 。解题思路: 可以用双指针:右指针 right 指向当前将要处理的元素,左指针left 指向下一个将要赋值的位置; 如果右指针指向的元素不等于 val,则为输出数组的一个元素,将右指针指向的元素复制到左指针位置,然后将左右指针同时右移; 如果右指针指向的元素等于 val,它不能在输出数组里,此时左指针不动,右指针右移一位。 整个过程保持不变的性质是:区间 [0,left) 中的元素都不等于 val。 当左右指针遍历完输入数组以后,left的值就是输出数组的长度。
Ⅱ.4.5 双指针 - 删除有序数组中的重复项 (LeetCode26)
Ⅱ.4.6 双指针 - 删除有序数组中的重复项II (LeetCode80)
nums[ slow - 2 ] != nums[ fast ]
Ⅱ.4.7 双指针 - 颜色分类 (LeetCode75)
方法一:计数排序
两次遍历, 第一次统计0,1,2 出现的次数, 第二次按顺序生成数组
方法二:双指针
思路:① 快慢指针,初始化时指向头部; ② 遍历时一个在前一个在后
● 时间复杂度:O(n),其中n是数组nums的长度;
● 空间复杂度:O(1) 。用两个指针分别用来交换 0 和 2,指针 P0 来交换 0,P2 来交换 2, 初始值 P0 为 0, P2 初始值为 n-1 P2是从右向左移动的,故从左向右遍历整个数组时,当遍历的位置超过 P2 遍历即可停止从左向右遍历整个数组, 设当前遍历到的位置为 i,对应的元素为 nums[i] 如果找到了 0,将其与 nums[P0] 进行交换,并将 P0 向后移动一个位置 如果找到了 2,将其与 nums[P2] 进行交换,并将 P2 向前移动一个位置
Ⅱ.4.8 对撞指针 - 两数之和 II - 输入有序数组 (LeetCode167)
思路1: 暴利解法 O(n^2 )
思路2:利用有序数组这一前提,二分查找
思路3:双指针,对撞指针。
每次计算两个指针指向的两个元素之和,并和目标值比较。 如果两个元素之和等于目标值,则发现了唯一解。 如果两个元素之和小于目标值,则将左侧指针右移一位。 如果两个元素之和大于目标值,则将右侧指针左移一位。
Ⅱ.4.9 对撞指针 - 移除元素 (LeetCode27)
① 对撞指针,或者叫左右指针,初始化时指向头尾
② 遍历时一个从前向后,另一个从后向前③ 对撞指针适用于有序数组
Ⅱ.4.10 滑动窗口 - 长度最小的子数组 (LeetCode209)
思路1: 暴力解法 O(n^2)
思路2: 滑动窗口 O(n)
滑动窗口是在给定窗口大小的数组或字符串上执行要求的操作
可将部分问题中的嵌套循环变为单循环
适合解决数组, 字符串等问题
Ⅱ.4.11 查找 - 两个数组的交集 (LeetCode349)
思路一 :排序 + 双指针
时间复杂度:O(mlogm+nlogn)
思路二:利用set
时间复杂度:O(m+n)
Ⅱ.4.12 查找 - 两个数组的交集 II (LeetCode350)
思路一 :排序 + 双指针
时间复杂度:O(mlogm+nlogn)
思路二:利用字典
● 时间复杂度:O(m+n),其中 m 和 n 分别是两个数组的长度
● 空间复杂度:O(min(m,n)),其中 m 和 n 分别是两个数组的长度
Ⅱ.4.13 查找 - 存在重复元素 II (LeetCode 219)
解法1 字典
时间复杂度:O(n)
空间复杂度:O(n)解题思路: 创建字典用于查找, 遍历的过程中,判断在字典中是否存在满足条件的key, 如果满足直接返回True, 如不满足,则将数组中元素不断放入字典中, key为数值, value为索引
解法2 滑动窗口
时间复杂度:O(n)
空间复杂度:O(n)解题思路: 创建一个set 用来保存窗口中数据, 当扩大窗口时, 如果从set中能取到新加入的值, 说明有满足条件的元素
Ⅱ.5 链表
链表(Linked list)是一种常见的基础数据结,是由一组不必相连【不必相连:可以连续也可以不连续】的内存结构【节点】,按特定的顺序链接在一起的抽象数据类型。 其中,单链表第一个节点(Node)一般被称为 Head 。最后一个节点的Next属性必须指向 None ,表明是链表的结尾。
链表中的元素称为节点(node),每一个节点都有两个属性:
- 一个用来记录当前节点中保存的数据
- 一个用来记录下一个节点的引用
链表和数组的区别:
在大多数编程语言中,链表和数组在内存中的存储方式存在明显差异。数组要求内存空间是连续的,链表可以不连续。然而,在 Python 中,list是动态数组。所以在Python中列表和链表的内存使用非常相似。
链表和数组在以下的操作中也有本质区别:
- 插入元素:数组中插入元素时,插入位置之后的所有元素都需要往后移动一位,所以数组中插入元素最坏时间复杂度是O(n); 链表可以达到 O(1) 的时间复杂度
- 删除元素:数组需要将删除位置之后的元素全部往前移动一位,最坏时间复杂度是O(n); 链表可以达到 O(1) 的时间复杂度
- 随机访问元素:数组可以通过下标直接访问元素,时间复杂度为O(1); 链表需要从头结点开始遍历,时间复杂度为O(n)
- 获取长度: 数组获取长度的时间复杂度为O(1); 链表获取长度也只能从头开始遍历,时间复杂度为O(n)
常用链表:
● 单链表,两个属性,一个记录数据,一个记录下一个元素;
● 双向链表,顾名思义,Node中除了记录下一个元素(next)之外, 还增加一个属性记录上一个元素;
● 循环链表,将单链表中最后一个Node的next 属性, 指向头节点(head),即为循环链表。
注意
- 解决链表问题时,需要注意边界问题
- 灵活使用虚拟头结点
Ⅱ.5.1 反转链表 (LeetCode 206)
Ⅱ.5.2 移除链表元素 (LeetCode 203)
时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。
空间复杂度:O(1)
注意 由于链表的头节点 head 有可能需要被删除,
因此创建哑节点 dummyHead,令 dummyHead.next=head,初始化temp=dummyHead,
然后遍历链表进行删除操作。最终返回 dummyHead.next即为删除操作后的头节点。
Ⅱ.5.3 两两交换链表中的节点 (LeetCode 24)
时间复杂度:O(n),其中 n 是链表的长度
空间复杂度:O(1)
Ⅱ.5.4 删除链表中的节点 (LeetCode 237)
思路分析:
我们在做链表元素删除时, 一般的思路是,
① 找到要删除节点的上一个节点
② 将上一个节点的next属性 指向要删除节点的下一个节点然而,当前题目中的要求是,我们无法访问头结点, 那么就无法得知要删除节点的上一个节点,但是能够获取要删除节点的下一个节点,可以利用它来完成我们的需求 (把待删除节点的下一个节点的值付给它,然后删除下一个节点)。
Ⅱ.5.5 删除链表中的倒数第N个节点 (LeetCode 19)
解法1:
时间复杂度:O(n),其中n是链表的长度;
空间复杂度:O(1)思路分析: 首先遍历获取长度,创建一个虚拟头结点,从虚拟头结点开始遍历, L−n+1 个节点。 当遍历到第L−n+1个节点时,它的下一个节点就是我们需要删除的节点。
解法2
时间复杂度:O(n),其中 n 是链表的长度;
空间复杂度:O(1)思路分析: 可以使用两个指针 first 和 second 同时对链表进行遍历,并且 first 比 second超前 n 个节点。 当first遍历到链表的末尾时,second就恰好处于倒数第 n 个节点。
Ⅱ.6 栈、堆
栈 (stack),是一种以后进先出的方式存取数据的数据结构(LIFO Last-In/First-Out)
可以通过编辑器中的撤销(undo)功能来加深对栈的理解。
● 从栈中添加元素的操作称为 push;
● 从栈中移除元素的操作称为 pop。
队列 (queue),是一种以先进先出的方式存取数据的数据结构(FIFO First-In/First-Out)
Queue 的特点类似一个管子, 我们从管子的一端放入元素, 从另一端取出元素。
● 向队列中添加元素的操作称为 Enqueue;
● 从队列中移除元素的操作称为 Dequeue。
Ⅱ.6.1 栈 - 有效的括号 (Leetcode 20)
时间复杂度:O(n)
空间复杂度:O(n)
Ⅱ.6.2 栈 - 最小栈 (Leetcode 155)
Ⅱ.6.3 队 - 数据流中的移动平均值 (Leetcode 346)
思路一: 利用数组/列表
思路二: 利用队列
Ⅱ.6.4 队 - 用队列实现栈 (Leetcode 225)
Ⅱ.6.5 队 - 滑动窗口最大值 (Leetcode 239)
解法一: 优先队列
优先队列:出队顺序与入队顺序无关,和优先级有关
普通队列:先进先出,后进后出
一般用最大堆/最小堆 来实现优先队列解题思路: 向优先队列(最大堆)中添加K个元素,此时队列头的元素为最大值; 通过一个list保存每次移动窗口对应的最大值; 滑动窗口时,添加新的元素到队列中, 如果队列头的元素已经划出了窗口,则需要将队列头元素Pop出去,保存到结果列表中
解法二: 单调递减队列
实际上没有必要维护一个长度为K的队列,我们的目标是获取最大值,可以去掉那些不可能成为窗口最大值的元素。为了方便判断队首元素与滑动窗口的位置关系,队列中保存的是对应元素的下标。解题思路: 初始时单调队列为空,随着对数组的遍历过程中,每次插入元素前,需要考察: 1、合法性检查:队头下标如果距离 i 超过了 k ,则应该出队。 2、单调性维护:如果 nums[i] 大于或等于队尾元素下标所对应的值,则当前队尾不可能成为最大值从队尾出队 3、如此遍历一遍数组,队头就是每个滑动窗口的最大值所在下标
Ⅱ.7 二叉树 - 递归、回溯
二叉树(Binary Tree) 是一种树形数据结构,其中每个父节点最多可以有两个子节点。二叉树的每个节点(node)包含三个属性:
● data 数据
● left 左子节点的地址
● right 右子节点的地址
说明:叶子节点是指没有子节点的节点。
二叉树天然的具有递归结构,二叉树的递归定义为:
- 二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;
- 左子树和右子树又同样都是二叉树。
回溯法也称试探法,是暴力解法的一个主要手段。它的基本思想是:
● 从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,
● 当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索直到所有的“路径”(状态)都试探过。
这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法” 。
Ⅱ.7.1 递归 - 二叉树的最大深度 (LeetCode 104)
解题思路:
如果我们知道了左子树和右子树的最大深度 l和 r,那么该二叉树的最大深度即为 max(l,r) + 1
● 时间复杂度:O(n) ,其中 n 为二叉树节点的个数。每个节点在递归中只被遍历一次。
● 空间复杂度:O( height) ,其中 height表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
Ⅱ.7.2 递归 - 翻转二叉树 (LeetCode 226)
时间复杂度:O(n)
空间复杂度:O(n)
Ⅱ.7.3 递归 - 路径总和 (LeetCode112)
注:使用递归的思路解决问题的时候, 需要注意递归的终止条件。
Ⅱ.7.4 递归 - 电话号码的字母组合 (LeetCode 17)
时间复杂度:O(3n) (O(2n))
空间复杂度:O(m+n)
Ⅱ.7.5 回溯 - 全排列 (LeetCode 46)
Ⅱ.8 动态规划、贪心算法
动态规划:把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,在解决小问题的时候,记忆每一个小问题的答案,使得每个小问题只解决一次,最终达到解决原问题的效果。
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择,就能得到问题的答案。贪心算法需要充分挖掘题目中条件,没有固定的模式,解决有贪心算法需要一定的直觉和经验。
Ⅱ.8.1 动态规划 - 爬楼梯 (LeetCode 70)
解题思路:
假设f(n) 表示爬 n 阶楼梯可能的走法。
自顶向下的思路: 假设当前站在第n阶楼梯上,
那么上一步可能在第n-1 或者 n-2 阶, 分别需要爬1级台阶和2级台阶所以 f(n) = f(n-1) + f(n-2)解法1 暴力搜索
上面式子中 n-2 > =0 所以 最后一次递归调用为 f(2) = f(1) + f(0),边界就是f(1) = 1,f(0) =1解法2 记忆化搜索
解法3 动态规划
Ⅱ.8.2 动态规划 - 整数拆分 (LeetCode 343)
Ⅱ.8.3 动态规划 - 打家劫舍 (LeetCode 198)
解题思路:
● 如只有一间房屋,则偷窃该房屋,可以偷到最高总金额。
● 如只有两间房屋,则由于两间房屋相邻,不能同时偷窃,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃,可以偷窃到最高总金额。
● 对于第 k (k>2) 间房屋,有两个选项:● 偷窃第 k 间房屋,那么就不能偷窃第 k-1 间房屋,偷窃总金额为前 k-2 间房屋的最高总金额与第k 间房屋的金额之和。
● 不偷窃第 k 间房屋,偷窃总金额为前 k-1 间房屋的最高总金额。在两个选项中选择偷窃总金额较大的选项,该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。
用 dp[i] 表示前 i 间房屋能偷窃到的最高总金额,那么就有如下的状态转移方程:
边界条件为:
时间复杂度:O(n)
空间复杂度:O(1)
Ⅱ.8.4 贪心算法 - 发饼干 (LeetCode 455)
思路分析: 为了尽可能满足最多数量的孩子,从贪心的角度考虑,
● 应该按照孩子的胃口从小到大的顺序依次满足每个孩子,
● 且对于每个孩子,应该选择可以满足这个孩子的胃口且尺寸最小的饼干。
时间复杂度:O(mlogm+nlogn)解题 首先对数组 g 和 s 排序,然后从小到大遍历 g 中的每个元素,对于每个元素找到能满足该元素的s 中的最小的元素。具体而言,令 i 是 g 的下标,j是 s 的下标,初始时 i 和 j 都为 0,进行如下操作。 对于每个元素 g[i],找到未被使用的最小的 j 使得 g[i] ≤ s[j],则 s[j] 可以满足 g[i]。 由于 g 和 s 已经排好序,因此整个过程只需要对数组 g 和 s 各遍历一次。 当两个数组之一遍历结束时,说明所有的孩子都被分配到了饼干,或者所有的饼干都已经被分配或被尝试分配(可能有些饼干无法分配给任何孩子),此时被分配到饼干的孩子数量即为可以满足的最多数量。
代码调试不易,转载请标明出处!
如果感觉本文对您有帮助,请留下您的赞,您的支持是我坚持写作分享的最大动力,谢谢!
References
0.数据结构与算法 | 菜鸟教程
1.C++ 教程 | 菜鸟教程
2.Python 教程
3.题库 - 力扣 (LeetCode)
4.题库 - 牛客网 (nowcoder)
可以肯定的是学海无涯,这篇文章也会随着对 VS、PCL 的深入学习而持续更新,
欢迎各位在评论区留言进行探讨交流。