本专栏已参加蓄力计划,感谢读者支持
往期文章
一. Java入门算法(贪心篇)丨蓄力计划
二. Java入门算法(暴力篇)丨蓄力计划
三. Java入门算法(排序篇)丨蓄力计划
四. Java入门算法(递归篇)丨蓄力计划
五. Java入门算法(双指针篇)丨蓄力计划
六. Java入门算法(数据结构篇)丨蓄力计划
七. Java入门算法(滑动窗口篇)丨蓄力计划
你好,我是Ayingzz,Ayi是我的名字,ing进行时代表我很有动力,zz提醒我按时睡觉 ~
- 篇幅短小精悍,适合初学者反复咀嚼:此专栏的文章并不是一系列大而全的整理文章,而是一系列简明扼要的算法入门讲解文章,篇幅短而内容精,有利于初学者针对一种或多种算法快速入门。
- 例题简单易懂,让你印象深刻:引入精选LeetCode简易算法例题,通过生动形象的讲解对其思路进行简明剖析,更容易上手并掌握。
- 涉及算法种类广:双指针、递归、排序、贪心、分治、动态规划、滑动窗口、DFS...各类基础算法收揽其中。
为什么要学算法?
对于所有的Problems-Solving的过程都可以理解为算法,程序员对算法或多或少都有着一些复杂的情感,为什么一定要学算法?
- "程序 = 数据结构 + 算法"。这个公式相信已经耳濡目染,目前在各大厂的面试里,对基础算法的考察的比重逐年增加,只写会某种语言的工程代码显然并不太够,大部分面试官会优先考虑掌握算法的面试者。在现实开发里,仅使用一些简单的算法就可以快读优化各种繁杂的工程代码,降低时间复杂度与工程运行速度,提升用户体验。
- 对算法的热爱。作为程序员或多或少对算法都有着某种情感上的执着与偏爱,如果你还是学生,想参与各类的竞赛,那么入门算法即是数学建模、软件开发、算法等各类竞赛的敲门砖,选手的动力就是对算法的追求与热爱,类似的有ACM、蓝桥杯、传智杯等。
专栏思路和内容大纲
基础部分:
- 双指针:巧用双指针、三指针完成搜索以及其他算法功能;
- 递归:递归是算法的敲门砖,大部分算法中都包含递归;
- 排序:基础入门排序算法,包括选择、插入、冒泡排序;
- 贪心:贪心选择性质、最优子问题讲解;
- 暴力枚举:常见的暴力枚举题目讲解,铺垫之后的优化;
- 数据结构:包括栈、哈希表等。
进阶部分:
- 动态规划:一张表可以解决的问题,进阶版递归;
- DFS与BFS:深度优先搜索、广度优先搜索;
- 滑动窗口:利用指针或数组维护特定区间的移动;
- 高级排序:排序用时短的高级排序,包括快速排序、归并排序、堆排序等;
- 分治:分而治之,对原问题的切割子问题求解;
- 回溯:以深度优先方式配合状态变量系统搜索问题解的算法。
适宜人群
- 对算法感兴趣的初学者
- 想加强算法基本功的读者
写在前面
我们都知道 “程序 = 数据结构 + 算法”,开发常用的数据结构有:数组、栈、链表、队列、树、图、堆、哈希表等,算法题需要用来辅助解题的数据结构也不在少数, 栈、哈希表(散列表)这两个结构在算法题中用得特别多,先来简单看看他们是什么:
- 栈:只能在其中一端进行操作的线性表(先进后出、后进先出)。想像一下手枪弹夹和子弹,每次装填子弹都从弹夹一端装入,第一发子弹会被压到弹夹底部,发射子弹时,最后装填的那一发子弹会作为第一发射出,这就是栈。Java常见的实现栈的结构有Stack(不推荐)、Deque(推荐使用)。
- 哈希表:保存了两个对象之间的映射关系(key-value)的一种集合,在Java里的实现有HashMap、HashSet等,前者允许加入重复的Key,后者则不允许,可以去搜一下这两个结构以及栈的具体原理讲解,这里不作赘述。
巧用数据结构能够帮助我们从另一个角度快速解题,下面讲解具体的应用。
本篇内容
MinStack(辅助栈)
LeetCode题目描述:155.最小栈(Easy)
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
- push(x) —— 将元素 x 推入栈中。
- pop() —— 删除栈顶的元素。
- top() —— 获取栈顶元素。
- getMin() —— 检索栈中的最小元素。
示例
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
若没有getMin()方法,这道题就是简单的实现一个栈的几个基本的操作:压栈、弹栈、获取栈顶元素。所以关键是怎么实现getMin()获取栈中最小值的这个功能,方法是有的,而且还不少:
- 遍历,搜索最小值
public int getMin() {
int min = Integer.MAX_VALUE;
for (Integer x : stack) {
if (min > x) {
min = x;
}
}
return min;
}
- 排序,获取排序后的栈顶元素
public int getMin() {
List<Integer> sortStack = new ArrayList<Integer>(stack);
Collections.sort(sortStack);
return sortStack.get(0);
}
实际上这两种方法的速度都太慢了,AC时间在300ms~600ms之间,所以有没有更快一点的方法呢?有。
- 设置辅助栈,与原始栈同步压栈、弹栈
在这一题中,辅助栈的作用是记录最小元素,将元素一起压进原始栈和辅助栈时,需要注意的是辅助栈压栈的方式是不同的。每次将要压栈(push)的元素与辅助栈的栈顶元素比较,取值较小的那一个加入辅助栈顶,这样就可以在辅助栈的栈顶一直记录着原始栈中值最小的那一个元素(细节)。
辅助栈的实现:
public class MinStack {
Deque<Integer> stack, minstack;
/** initialize your data structure here. */
public MinStack() {
stack = new ArrayDeque<Integer>();
// 辅助栈
minstack = new ArrayDeque<Integer>();
}
public void push(int val) {
stack.addFirst(val);
// 将要push的元素与辅助栈顶元素比较,取值小的那一个加入辅助栈顶
minstack.addFirst(minstack.isEmpty() ? val : Math.min(minstack.peekFirst(), val));
}
public void pop() {
stack.removeFirst();
// 同步pop,保持两个栈大小一样
minstack.removeFirst();
}
public int top() {
return stack.peekFirst();
}
public int getMin() {
// 取辅助栈顶元素即可
return minstack.getFirst();
}
}
当用到getMin()方法时,直接返回辅助栈的栈顶元素,它即是目前原始栈中的最小元素。借助辅助栈的方式实现最小栈,无疑是高效的,AC的时间在9ms左右,快了近50倍。
温度(单调栈)
LeetCode题目描述:739.每日温度(Middle)
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例: 给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
- 提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
本题可以使用暴力快慢指针解法,时间复杂度较高,不作介绍。
单调栈,意思是在解题过程中维护一个栈,使其中元素一直保持单调性。
在这一题,因为题目中求的是至少需要等待的天数,而不是温度差,固应将原数组下标作为元素依次加入栈中,代表天数 i 。接下来,我们将下一个即将加入栈的元素 i 与当前栈顶元素作判断:
- 如果栈顶元素对应的温度比 i 对应的温度大,那我们就找到了栈顶元素天数对应的温度第一次升温的天数,也就是 第 i 天,此时将栈顶元素出栈,计算其与 i 的差值即是在这一天观测更高的温度至少需要等待的天数。
- 如果栈顶元素对应的温度比 i 对应的温度小,或是栈空,将 i 入栈。
按照这些步骤,可以惊喜地发现我们的这些操作一直都是满足了栈内元素的单调性,固称之为单调栈。最后依然存在栈里的元素,代表着在数组中找不到下一个更高温度的天数,题目要求在返回数组总用0代替,因为初始化已为0,不作处理(细节)。
单调栈的使用:
class Solution {
public int[] dailyTemperatures(int[] T) {
Deque<Integer> stack = new ArrayDeque<>();
int[] res = new int[T.length];
// 维护一个单调栈
for (int i = 0; i < T.length; ++i) {
// 栈不空 且 第i天的温度比栈顶天数的温度大
while (!stack.isEmpty() && T[stack.peek()] < T[i]) {
// 弹栈、计算相差天数储存到返回数组
int idx = stack.pop();
res[idx] = i - idx;
}
// 第i天压栈
stack.push(i);
}
return res;
}
}
.
卡片(HashMap)
先补充说明一下HashMap与最后一题里HashSet的异同,HashMap保存的是一对对key-value键值对,HashSet保存的是单一无重复的对象,它的底层封装了一个HashMap,但把value置为了常量,只对传进来的key对象做操作。
卡片这一题是第十二届蓝桥杯省赛中的题目。
最笨拙的办法,就是用代码模拟这一个拼卡片的过程,笨拙但有效。我们将0~9这些卡片作为Key,与2021作为初始的Value表示卡片剩余的数量组成键值对,依次存进HashMap中。
开始模拟,用 i 不断自增,作为我们当前拼的数字,每一次将它的位数分解,去HashMap里找对应的Value值,若Value值不为0,那么就表示这种卡片还有,有我们就用,用了之后对应的就是那一种卡片的Value值 - 1。当检测到某一个Value值变为了 0,意味着组成当前拼的这一个数字的某一个位数的数字卡片没有了,所以拼不了了,但是 i 已经自增,所以 i - 1 就是我们的答案(细节)。
HashMap的使用:
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
// put key 0~9 到哈希表里
for (int i = 0; i < 10; ++i) {
map.put(i, 2021);
}
// 暴力,从1开始自增
for (int i = 1; ; ++i) {
int n = i;
// 取拼成 i 所需的每一个数字 m
while (n > 0) {
int m = n % 10;
if (map.get(m) > 0) {
// 只要m卡片还有,就用,用完后对应value值 - 1
map.put(m, map.get(m) - 1);
}else{
// 当哈希表中某个 Value 变为 0 时,输出
System.out.println(i - 1);
return;
}
n /= 10;
}
}
}
}
(运行结果:3181)
分糖果(HashSet)
LeetCode题目描述:575. 分糖果(Easy)
给定一个偶数长度的数组,其中不同的数字代表着不同种类的糖果,每一个数字代表一个糖果。你需要把这些糖果平均分给一个弟弟和一个妹妹。返回妹妹可以获得的最大糖果的种类数。
示例
输入: candies = [1,1,2,2,3,3]
输出: 3
解析: 一共有三种种类的糖果,每一种都有两个。
最优分配方案:妹妹获得[1,2,3],弟弟也获得[1,2,3]。这样使妹妹获得糖果的种类数最多
-----------------------------------------------------------------------------------
输入: candies = [1,1,2,3]
输出: 2
解析: 妹妹获得糖果[2,3],弟弟获得糖果[1,1],妹妹有两种不同的糖果,弟弟只有一种。
这样使得妹妹可以获得的糖果种类数最多。
偶数长度,表示可以平均分。
不同数字代表不同种类的糖果,每个数字代表一个糖果,假设数字不重复,糖果的种类就是数组的长度,但题目需要将糖果平均分给弟弟和妹妹,所以妹妹实际获得的糖果最多只有 n / 2;假设糖果的种类远小于糖果的数量,也就是数组中有一些糖果是重复的,此时应将计算出的实际种类数与数组长度的一半做比较,妹妹最多可以获得的糖果种类就是两者中的较小者。
计算种类数,我们用HashSet(不允许加入重复的键)来实现,只需将数组中所有元素依次加入Set,不管有没有重复,最终得到的Set集合里都不会存在重复的key,固Set的大小Size即是我们要求的种类数。将其与数组长度的一半取小者返回即可。
HashSet的使用:
public class Solution {
public int distributeCandies(int[] candies) {
HashSet < Integer > set = new HashSet < > ();
// Set中不允许含有重复元素
for (int candy: candies) {
set.add(candy);
}
// 最后Set里就是所有不同种类的糖果,注意弟弟妹妹需要平均分
return Math.min(set.size(), candies.length / 2);
}
}
推荐练习
\
股票价格跨度
- 编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。
- 今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
存在重复元素
- 给定一个整数数组,判断是否存在重复元素。
- 如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。
/
题目来源:https://leetcode-cn.com/
栈动图来源:https://www.jianshu.com/p/603f919b2693
本专栏持续更新,预计7月结束,感谢读者的支持