常用数据结构总结
1.1 常用数据结构和技巧
1.2数组、字符串/Array & String
优点
构建一个数组非常简单
能让我们在O(1)的时间里根据数组的下标(index)查询某个元素
缺点
构建时必须分配一段连续的空间
查询某个元素是否存在时需要遍历整个数组,耗费O(n)的时间(其中,n是元素的个数)
删除和添加某个元素时,同样需要耗时O(n)的时间
例题:
LeetCode 242. 有效的字母异位词
题目:给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例1:
输入: s = “anagram”, t = “nagaram”
输出: true
示例2:
输入: s = “rat”, t = “car”
输出: false
解题思路:
所谓字母异位词,就是两个字符串中的相同字符的数量要对应相等,例如,例一的s和t,此时为true。
分析处理:
题目中有个说明,假设两个字符串中都只包含小写字母。
法一:我们都知道,小写字母一共也就二十六个,这意味着我们可以利用两个长度都为26的字符数组,来统计每个字符串中小写字母出现的次数,然后再对比看看是否相等即可。
法二:可以只定义一个长度为26的字符数组,将出现在字符串s里的字符个数加一,出现在字符串t里的字符个数减一,最后判断每个小写字符的个数是否为0,若为0,返回true,反正false。
代码暂不提供,建议读者自己去写一下,不理解的可以私聊博主。
1.3链表/Linked-list
单链表 :链表中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。
双链表 :与单链表不同的是,双链表的每个结点都含有两个引用字段。
优点
灵活地分配内存空间
能在O(1)时间内删除或者添加元素
缺点
查询元素需要O(n)时间
解题技巧
1.利用快慢指针(有时候需要用到三个指针)
例如:
链表的反转
寻找倒数第k个元素
寻找链表中中间位置的元素
判断链表是否有环等等
2.构建一个虚假链表头
例如:
两个排序链表,进行排序整合
将链表中的奇偶数按原定顺序分离,生成前半部分为奇数,后半部分为偶数的链表。
如何训练该技巧
1.在纸上或者白板上画出节点之间的相互关系
2.画出修改的方法
3.这样可以有效的帮助你分析问题,凭空想象是比较困难的。
例题:
LeetCode 25. K 个一组翻转链表
题目:
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
示例 3:
输入:head = [1,2,3,4,5], k = 1
输出:[1,2,3,4,5]
示例 4:
输入:head = [1], k = 1
输出:[1]
解题思路:
这个题是对LeetCode24题的进阶,这个题考察了两个知识点:
1.你对链表翻转的熟悉程度
2.你对递归算法的理解程度
首先我们要确保给定的链表的长度是大于K的,这样我们才能继续,否则直接返回原链表就可以了。
然后,我们可以用三个指针prev、curr、next分别代表了前一个结点,当前结点和下一个结点。每次将curr指向的下一个结点保存到next指针,然后curr指针指向prev,接着curr和prev一起前进一步,接着重复这一步,直到把这组当中的k个元素翻转完毕。
当完成了局部的翻转后,prev就是新的链表的头,curr指向了下一个要被处理的局部,而原来的头指针head成为了链表的尾巴。
核心代码如下:
prev = null;
curr = head;
n = k;
while(curr && n-- > 0) {
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
1.4 栈/Stack
特点
后进先出(LIFO)
对于栈中的数据来说,所有操作都是在栈的顶部完成的,只可以查看栈顶部的数据,只能够从栈的顶部压入数据,也只能从栈的顶部弹出数据。
算法基本思想
可以⽤⼀个单链表来实现
只关⼼上⼀次的操作
处理完上⼀次的操作后,能在 O(1) 时间内查找到更前⼀次的操作
例题:
LeetCode 20. 有效的括号
题目:
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
示例 4:
输入:s = “([)]”
输出:false
示例 5:
输入:s = “{[]}”
输出:true
解题思路:
我们可以利用一个栈,不断的往里压左括号,一旦遇上右括号,我们就把栈顶的左括号弹出,表示这是一个合法的组合,以此类推,直至最后判断栈里面还有没有左括号剩余。
例题2:
LeetCode第739题每日温度
题目:
请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例 2:
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
示例 3:
输入: temperatures = [30,60,90]
输出: [1,1,0]
这里简单说一下解题思路:
设置一个递减栈:栈里只有递减元素
遍历整个数组,如果栈不空,且当前数字大于栈顶元素,那么如果直接入栈的话就不是 递减栈 ,所以需要取出栈顶元素,由于当前数字大于栈顶元素的数字,而且一定是第一个大于栈顶元素的数,直接求出下标差就是二者的距离。
继续看新的栈顶元素,直到当前数字小于等于栈顶元素停止,然后将数字入栈,这样就可以一直保持递减栈,且每个数字和第一个大于它的数的距离也可以算出来。
1.4 队列/Queue
特点
先进先出(FIFO)
对于队列的数据来说,我们只允许在队尾查看和添加数据,在对头查看和删除数据。如何实现一个队列呢,我们可以借助双链表,双链表的头指针允许我们在对头查看和删除数据,尾指针允许我们在队尾查看和添加数据
常用的场景
广度优先搜索(后面会详细介绍)
1.5 双端队列/Deque
基本实现
可以利⽤⼀个双链表
队列的头尾两端能在 O(1) 的时间内进⾏数据的查看、添加和删除
常⽤的场景
实现⼀个⻓度动态变化的窗⼝或者连续区间
题目:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
示例 3:
输入:nums = [1,-1], k = 1
输出:[1,-1]
示例 4:
输入:nums = [9,11], k = 2
输出:[11]
示例 5:
输入:nums = [4,-2], k = 2
输出:[4]
解题思路:
最直观的解法:
每次都要在移动的窗口中找到最大值,那很简单,我们就移动这个窗口,然后扫描一遍窗口获得最大值。假设数组里有n个元素,那么算法复杂度是O(n*k)。
优化解法:
利用双端队列来表示这个窗口,这个双端队列保存当前窗口中最大的那个数的下标,双端队列新的头总是当前窗口中最大的那个数,同时有了这个下标,我们可以很快知道新的窗口是否已经不在包含原来那个最大的数,如果不在包含,我们就把旧的数从双端队列的头删除。按照这种操作,不管窗口的长度是多长,因为数组里的每一个数,都分别被压入和弹出双端队列一次,所以我们可以在O(n)的时间里完成任务。
1.6 树/Tree
树的共性
结构直观
通过树问题来考察
递归算法
掌握的熟练程度
⾯试中常考的树的形状有
普通⼆叉树
平衡⼆叉树
完全⼆叉树
⼆叉搜索树
四叉树
多叉树
特殊的树:
红⿊树、⾃平衡⼆叉搜索树
主要考察
遍历
前序遍历(Preorder Traversal)
中序遍历(Inorder Traversal)
后序遍历(Postorder Traversal)
以及三种遍历的递归写法和复杂度分析。
例题
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
解题思路:
考察知识点:
二叉搜索树具有如下性质:
1.结点的左子树只包含小于当前结点的数。
2.结点的右子树只包含大于当前结点的数。
3.所有左子树和右子树自身必须也是二叉搜索树。
二叉树的中序遍历即按照访问左子树——根结点——右子树的方式遍历二叉树;在访问其左子树和右子树时,我们也按照同样的方式遍历;直到遍历完整棵树。
操作:
因为二叉搜索树和中序遍历的性质,所以二叉搜索树的中序遍历是按照键增加的顺序进行的。于是,我们可以通过中序遍历找到第 k 个最小元素。
同理,这道题可以求第k个最大元素,同样的解题思路,只是把中序遍历进行倒序,就从递增遍历变成了递减遍历。