图解数据结构(开篇)

图解数据结构

  • 做者:天行

参加了 lucifer 的 91 天学算法活动,不知不觉中已经一月有余。从盲目地作到有目的、有套路地去作。git

在 lucifer 的 91 课程中,从基础到进阶到专题,在这个月中,经历了基础篇的洗礼,无论在作题思路,仍是作题速度都有了很大的提高,这个课程,没什么好说的,点赞点赞再点赞。也意识到学习好数据结构有多重要,不只是思惟方式的改变,仍是在工程上的应用。github

对一个问题使用画图、举例、分解这 3 种方法将其化繁为简,造成清晰思路再动手写代码,一张好的图可以更好地帮助去理解一个算法。所以本次分享如何使用画图同时结合经典的题目的方法去阐述数据结构。面试

​<!-- more -->算法

数据结构与算法有用么?

这里我摘录了一个知乎的高赞回答给你们作参考:编程

我的认为数据结构是编程最重要的基本功没有之一!学了顺序表和链表,你就知道,在查询操做更多的程序中,你应该用顺序表;而修改操做更多的程序中,你要使用链表;而单向链表不方便怎么办,每次都从头至尾好麻烦啊,怎么办?你这时就会想到双向链表 or 循环链表。
学了栈以后,你就知道,不少涉及后入先出的问题,例如函数递归就是个栈模型、Android 的屏幕跳转就用到栈,不少相似的东西,你就会第一时间想到:我会用这东西来去写算法实现这个功能。
学了队列以后,你就知道,对于先入先出要排队的问题,你就要用到队列,例如多个网络下载任务,我该怎么去调度它们去得到网络资源呢?再例如操做系统的进程(or 线程)调度,我该怎么去分配资源(像 CPU)给多个任务呢?确定不能所有一块儿拥有的,资源只有一个,那就要排队!那么怎么排队呢?用普通的队列?可是对于那些优先级高的线程怎么办?那也太共产主义了吧,这时,你就会想到了优先队列,优先队列怎么实现?用堆,而后你就有疑问了,堆是啥玩意?本身查吧,敲累了。
总之好好学数据结构就对了。我以为数据结构就至关于:我塞牙了,那么就要用到牙签这“数据结构”,固然你用指甲也行,只不过“性能”没那么好;我要拧螺母,确定用扳手这个“数据结构”,固然你用钳子也行,只不过也没那么好用。学习数据结构,就是为了了解之后在 IT 行业里搬砖须要用到什么工具,这些工具备什么利弊,应用于什么场景。之后用的过程当中,你会发现这些基础的“工具”也存在着一些缺陷,你不知足于此工具,此时,你就开始本身在这些数据结构的基础上加以改造,这就叫作自定义数据结构。并且,你之后还会造出不少其余应用于实际场景的数据结构。。你用这些数据结构去造轮子,不知不觉,你成了又一个轮子哥。

既然这么有用,那咱们怎么学习呢?个人建议是先把常见的数据结构学个大概,而后开始安装专题的形式突破算法。这篇文章就是给你们快速过一下一部分常见的数据结构。数组

从逻辑上分,数据结构分为线性和非线性两大类。网络

  • 线性数据结构包括数组、链表、栈、队列。
  • 非线性结构包括树、哈希表、堆、图。

而咱们经常使用的数据结构主要是数组、链表、栈、树,这同时也是本文要讲的内容。数据结构

数据结构一览

数组

数组的定义为存放在连续内存空间上的相同类型数据的集合。由于内存空间连续,因此能在 O(1)的时间进行存取。函数

剑指 offer03.数组中的重复的数字

题目描述:工具

在一个长度为 n 的数组 nums 里的全部数字都在 0 ~ n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每一个数字重复了几回。请找出数组中任意一个重复的数字。

分析:

重复意味至少出现两次,那么找重复就变成了统计数字出现的频率了。那如何统计数字频率呢?(不使用哈希表),咱们能够开辟一个长度为 n 的数组 count_nums,而且初始化为 0,遍历数组 nums,使用 nums[i]为 count_nums 赋值.

图解:

image

(注意:数组下标从 0 开始)

剑指 offer21. 调整数组顺序使奇数位于偶数前面

题目描述:

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得全部奇数位于数组的前半部分,全部偶数位于数组的后半部分。

分析:

根据题目要求,须要咱们调整数组中奇偶数的顺序,那这样的话,咱们能够从数组的两端同时开始遍历,右边遇到奇数的时候停下,左边遇到偶数的时候停下,而后进行交换。

image

1122.数组的相对排序

题目描述:

给你两个数组,arr1 和  arr2,
arr2  中的元素各不相同
arr2 中的每一个元素都出如今  arr1  中
对 arr1  中的元素进行排序,使 arr1 中项的相对顺序和  arr2  中的相对顺序相同。未在  arr2  中出现过的元素须要按照升序放在  arr1  的末尾。

示例

输入:arr1 = [2,3,1,3,2,4,6,7,9,2], arr2 = [2,1,4,3,9,6]

输出:[2,2,2,1,4,3,3,9,6,7]

分析:

观察输出,发现数字,由于 arr1 老是根据 arr2 中元素的相对大小来排序,因此只至关于在 arr2 中进行填充,每一个地方该填充多少呢?这个时候就须要去统计 arr1 中每一个数字出现的频率。

image

小结

在数组中,由于数组是一个有序的结构,这里的有序是指在位置上的有序,因此大多数只须要考虑顺序或者相对顺序便可。

链表

链表是一种线性数据结构,其中的每一个元素其实是一个单独的对象,每个节点里存到下一个节点的指针(Pointer)。
就像咱们玩的寻宝游戏同样,当咱们找到一个宝箱的时候,里面还存在寻找下一个宝箱的藏宝图,依次类推,每个宝箱都是如此,一直到找到最终宝藏。

image

经过单链表,能够衍生出循环链表,双向链表等。

咱们来看下链表中比较经典的几个题目。

面试题 02.02. 返回倒数第 k 个节点

题目描述:

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

示例:

输入: 1->2->3->4->5 和 k = 2
输出: 4

分析:

想要找到倒数第 k 个节点,若是此时在数组中,那咱们只须要用最后一个数组的索引减去 k 就能找到这个值,可是链表是不能直接经过索引获得的。若是此时,咱们知道最后一个节点的位置,而后往前找 k 个不就找到咱们须要的节点了吗?等价于咱们要找的节点和最后一个节点相隔 k 个位置。因此当有一个指针 front 出发 k 步后,咱们再出发,等 front 到达终点时,咱们恰好到达倒数第 k 个节点。

image

咱们把这种解法叫作双指针,或者快慢指针,或者先后指针,这种方法能够用于寻找链表中间节点,判断是链表中是否存在环(循环链表)并寻找环入口。

61. 旋转链表

题目描述:

给定一个链表,旋转链表,将链表每一个节点向右移动 k 个位置,其中 k 是非负数。

示例:

输入: 1->2->3->4->5->NULL, k = 2

输出: 4->5->1->2->3->NULL

分析:

每一个数字向后移动 k 位,那么最后 k 位就须要移动到前面,和找倒数第 k 位数字很类似,k 位后面的都移到开头。惟一须要注意的地方就是,k 的值可能大于链表长度的 2 倍及以上,因此须要算出链表的长度,以保证尽快找到倒数 k 的位置。

解法 1

找到位置后,直接断开
image

解法 2

制做循环链表,而后再找倒数第 k 个数,而后断开循环链表
image

24. 两两交换链表中的节点

题目描述:

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是须要实际的进行节点交换。

示例:

输入:head = [1,2,3,4]

输出:[2,1,4,3]

分析:

原理很简单,两个指针,分别指向相邻的两个节点,而后再添加一个临时指针作换交换的中介添加 dummy 节点,不用考虑头节点的状况,更加方便。直接上图:

image

除了同时操做一个链表以外,有的题目也会给出两个或者更多的链表,如两数相加,如 leetcode 中 2.两数相加、21.合并两个有序链表、160.相交链表

21.相交节点

题目描述:

编写一个程序,找到两个单链表相交的起始节点。

以下面的两个链表

image

分析:

咱们知道,对于任意两个数 ab,必定有 a+b-c=b+a-c,

image

基于 a+b-c=b+a-c,咱们能够设置两个指针,分别指向 A 和 B,以相同的步长同时移动,并在第一次到达尾节点的时候,指向另外一个链表,若是存在相交节点,也就是说 c > 0,那么两个指针必定会相遇,相遇处也就是相交节点。而当不存在时,即 c=0,那么两个指针最终都会指向空节点。

小结

链表中的操做无非就是两种,插入,删除。解题方法无非就是添加 dummy 节点(解决头节点的判断问题)、快慢指针(快慢不必定是单次步长同样,应该理解为平均步长,即便用了相同的时间,走的路程的长度来定义快慢)。

栈是一种先进后出(FILO, First In Last Out)的数据结构
能够把栈理解为

<center>

image

</center>

没错,就是上图的罐装薯片,想要吃到最底下的那片,必须依次吃完上面的。而在装薯片的时候,最底下的反而是最早装进去的。

在 leetcode 里面关于栈比较经典的题目有:20.有效的括号;150.逆波兰表达式求值

20.有效的括号

题目描述:

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

示例:
"{[()][]}()"

| 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 9   |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| {   | [   | (   | )   | ]   | [   | ]   | }   | (   | )   |

分析:

  • 一个有效的括号为,右边必须和左边对应,且存在至少一对有效括号的索引为[i, i+1]。 那么,咱们只要是括号左边部分,就入栈,右边部分,就和栈顶元素比较。

图解:

image

150.逆波兰表达式求值

题目描述:

根据 逆波兰表示法,求表达式的值。
有效的运算符包括 +, -, \*, / 。每一个运算对象能够是整数,也能够是另外一个逆波兰表达式。

示例:
["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+"]

分析:

  • 根据运算法则,咱们能够知道,一个运算有 num1,operation,num2 三部分组成。在一个逆波兰表达式中,运算符前面两个 num 就是这个运算的组成。
  • 咱们要作事情就是,找到一个运算符的时候,同时找到他前面的两个数,而栈的现金先去特性知足这个需求,使用栈来解决。

image

227.基本计算器 II

题目描述:

实现一个基本的计算器来计算一个简单的字符串表达式的值。字符串表达式仅包含非负整数,+, - ,\*,/ 四种运算符和空格 。 整数除法仅保留整数部分。

示例:3+5\*2/2-3

分析:

  • 与逆波兰表达式不一样的地方是,这里运算符两边是操做数。可是,这又有什么问题呢?万变不离核心,咱们只须要在找到运算符的同时,获得运算符两边的操做数。问题来了,还须要考虑运算符的优先级,想到的一个方法就是,只进行乘除法运算,最后进行加法运算,不进行减法运算(减去一个数 ⟺ 加上这个数的负数)

image

  • 若是可以把这个字符串表达式类似地转换位逆波兰表达式,那就能直接套用逆波兰表达式的代码了,回顾一下,逆波兰表达式是,每次有操做符的时候,就从栈顶出来两个元素。能够经过使用两个栈来实现,一个栈用来存储操做数,一个栈用来存储操做符。若是比栈顶的操做符符优先级低或者相同,那么就从操做符栈取出栈顶运算符号

image

496.下一个更大元素 I

题目描述:

给定两个 没有重复元素的数组  nums1 和  nums2 ,其中 nums1  是  nums2  的子集。找到  nums1  中每一个元素在  nums2  中的下一个比其大的值。nums1  中数字  x  的下一个更大元素是指  x  在  nums2  中对应位置的右边的第一个比  x  大的元素。若是不存在,对应位置输出 -1。

示例:

输入: nums1 = [4,1,2], nums2 = [1,3,4,2].

输出: [-1,3,-1]

分析:

题目要求咱们找出 nums1 中每一个元素在 nums2 中的下一个比这个元素大的值,又提到 nums1 是 nums2 的一个子集,咱们不妨找出 nums2 中每一个元素的下一个比他大的值,而后映射到 nums1 中。

那如何找出 nums2 中每一个元素的下一个比他大的值呢?对于当前元素,若下一个元素比他大,则找到了,不然的话,就把这个元素添加到栈中,一直到找到一个元素比栈顶元素大,这时候把栈里面全部小于这个元素的元素都出栈,听起来很绕,无妨,看图---->
image

最后栈中依然有数据存在,这是为何呢?由于这些元素后面找不到比他更大的值了。观察示例数据,4 后面没有比他更大的值了,5 和 1 也是。咱们还能观察到栈中元素是从大到小的,能够称这个栈为==单调递减栈==(如 1019.寻找链表中的下一个更大节点,503.下一个更大元素 II、402.移掉 k 位数字,39.每日温度,在 1673.找出最具备竞争力的子序列中,其实只须要构建一个单调递增栈,而后截取前 k 个。)。

回到题目,须要找到 nums1 中元素在 nums2 中的下一个比其大的值,只须要在刚才保存的信息中进行查找,找不到的则不存在,可使用哈比表保存每一个数对应的下一个较大值。

小结

栈因为其随时能够出栈和进栈,产生很是多的组合,带来了很是多的变化,因此读懂题目很是重要,而后选择方法,正所谓题目是有限的,方法是有限的。因此紧跟 lucifer 大佬学习套路,是一条值得坚持的道路,毕竟自古深情留不住,惟有套路得人心,这里推荐 lucifer 大佬的《单调栈模板带你秒杀八道题》,带你乱杀。

树虽相比于链表来讲,至少有两个节点(n 个节点就叫 n 叉树),可是树是一个抽象的概念,能够理解为一个不停作选择的过程,给定一个起始条件,会产生多种结果,而这些结果又成为新的条件,以此类推,直到再也不有新的条件。在树种,起始条件就是根节点,再也不产生新的条件的就是叶子节点。在树种,使用较多的是二叉树。一颗二叉树无论有多大,咱们均可以把他拆分为五种形式,

image

无论是在树上进行什么操做,都须要进行遍历,遍历的方式有两种:广度优先遍历(BFS)和深度优先遍历(DFS)。
简单来讲,广度就是先找到有多少种可能,而后找出这些可能有多少种可能;而深度就是每次只根据一个条件来找,直到最终没有条件。
话很少说,上图。

image

若是是试错的话,广度是一次把全部的结果都试一试,深度则是一条路走到黑。

这里直接借用 lucifer 大佬的广度、深度优先遍历模板(套路)

function dfs(root) {
 if (知足特定条件){
  // 返回结果 or 退出搜索空间
 }
 for (const child of root.children) {
        dfs(child)
 }
}

深度优先遍历根据逻辑处理(==敲黑板,很重要==)的前后分为前序遍历、中序遍历、后序遍历

// 前序遍历
function dfs(root) {
 if (知足特定条件){
  // 返回结果 or 退出搜索空间
    }
    // 主要逻辑
    dfs(root.left)
    dfs(root.right)
//中序遍历
function dfs(root) {
 if (知足特定条件){
  // 返回结果 or 退出搜索空间
    }
    dfs(root.left)
    // 主要逻辑
    dfs(root.right)
// 后序遍历
function dfs(root) {
 if (知足特定条件){
  // 返回结果 or 退出搜索空间
    }
    dfs(root.left)
    dfs(root.right)
    // 主要逻辑
}
}

接下来就要实操了

199. 二叉树的右视图

题目描述:

给定一棵二叉树,想象本身站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例:

输入: [1,2,3,null,5,null,4]

输出: [1, 3, 4]

解释:

image

分析:

此题便可以使用广度优先,也能够深度优先。
使用广度优先,只须要将每一层的节点用一个数组保存下来,而后输出最后一个
使用深度优先,这里我使用的是根右左的方式,这样能保证在每进入到一个新的层时,第一个访问到的就是最右边的元素。

上图:

image

112. 路径总和

题目描述:

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上全部节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。

示例:
image

分析:

求一条路径(根节点到叶子节点),这不就一条路走到底吗,没什么好犹豫的,选择深度优先遍历。由于须要得到路径上的和,咱们须要把每一个节点的值(状态)传递给下一个节点。

image

在 113. 路径总和 II 中,和本题相似,只须要把节点加入到数组中传递给下一个节点;在 129. 求根到叶子节点数字之和,须要把当前值*10 传递给下一个节点。

662. 二叉树最大宽度

题目描述:

给定一个二叉树,编写一个函数来获取这个树的最大宽度。树的宽度是全部层中的最大宽度。这个二叉树与满二叉树(full binary tree)结构相同,但一些节点为空。
每一层的宽度被定义为两个端点(该层最左和最右的非空节点,两端点间的 null 节点也计入长度)之间的长度。

示例:

image

分析:

最大宽度,不就是找出哪一层最长吗?广度优先搜索会更加方便,须要注意的是,非两端节点的 null 节点也要算到长度中,因此如今每一层存储的不只仅是有值节点。

上图

image

513. 找树左下角的值

题目描述:

给定一个二叉树,在树的最后一行找到最左边的值。

示例:

<center>

image

</center>
分析:
两个关键信息,一个最后一行,一个最左边。好像广度,深度均可以找到,在这里以深度进行说明,最后一行就是depth最大的,因此在深度遍历的时候,每次给一层传递depth信息。

image

与此题相似的还有 111. 二叉树的最小深度,104.二叉树的最大深度

感受,就这?好像也没什么难的啊,学完 lucifer 的课程,我就是这么膨胀。

小结

无非就是,深度遍历时,是否传递信息给下一层,给下一层传递什么信息;广度遍历时,是否保存每一层,是否保存空节点。

总结

本次给你们介绍了四种比较常见的数据结构,分别是数组,链表,栈和树。这四种只有树是逻辑上的非线性数据结构,由于一个节点可能有多个孩子,而其余数据结构只有一个前驱和一个后继。

因为先进后出的特性,咱们能够用数组轻松地在 $O(1)$ 时间复杂度模拟栈的操做。可是队列就没那么好命了,咱们必须使用链表来优化时间复杂度。

链表的考题相对比较单一,只要记住那几个点就行了。

树的题目比较丰富,和它的非线性数据结构有很大关系。因为其是非线性的,所以有了各类遍历方式,常见的是广度优先和深度优先,不少题目都是灵活运用这两种遍历方式问题就迎刃而解。

关注公众号力扣加加,学习算法不迷路。

参考:

相关文章
相关标签/搜索