目录
1、题目:242. 有效的字母异位词 - 力扣(LeetCode)
1、题目:349. 两个数组的交集 - 力扣(LeetCode)
1、题目:454. 四数相加 II - 力扣(LeetCode)
一、哈希表理论基础
1、哈希表、哈希函数、哈希碰撞
- 定义:哈希表(hash table)是根据关键码的值而直接进行访问的数据结构。
其实数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素。哈希表可以用来快速判断一个元素是否出现在集合里时间复杂度只要O(1)(枚举的话复杂度是O(n),比如查询一个名字是否在一个学校里)
- 哈希函数:形成映射关系。比如name可以通过哈希函数映射为index索引。(可以把其他数据格式转化为不同的数值,放在哈希表里面,一般都是有特定的编码方式的)
- 为了保证哈希函数得到的index都能在tablesize的范围内,就进行一个取模的操作。
- 哈希碰撞:如果姓名的数量本来就大于tabelsize,这样取模也没办法了。哈希碰撞是指:比如小李和小张都映射到了索引下标1的位置,就碰撞了。有两种解决方式:
- 1、拉链法:左图。把发生冲突的元素都存储到链表中,可以通过索引找到2个元素了。拉链法要选择适当的哈希表大小,防止因数组空值而浪费内存,或因链表太长而不易查找。
- 2、线性探测法:右图。一定要保证tablesize大于datasize。例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize 。
2、常见的三种哈希结构
当想使用哈希法来解决问题的时候,一般会选择这三种数据结构:数组、set集合、map映射。
数组就是刚刚上面将的那样,这里主要看一下set。
- 下面是C++中set的三种底层结构,std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
- 当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
- 下面是C++中map的三种底层结构,map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的
- std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的。(面试题)
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
简单来说,要是数值个数比较小,可控,就用数组。要是数值比较多,就用set。要是有key-value对应的话,就用map。
二、有效的字母异位词
1、题目:242. 有效的字母异位词 - 力扣(LeetCode)
如果字符串s和t中每个字符出现的次数都相同,就互为字母异位词,要判断t是否是s的字母异位词
2、思路
视频课:学透哈希表,数组使用有技巧!Leetcode:242.有效的字母异位词_哔哩哔哩_bilibili
最原始的暴力解法就是两层for循环,记录字符是否重复出现,时间复杂度是 O(n^2)。
- 由于题目中说了字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。定一个数组叫做record,大小为26 就可以了,记录出现的次数,遍历到就+1。
- 需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以映射下标为0-25。
- 在遍历s的时候,遇到了字符就+1,然后遍历t,遇到了字符就-1。检查最后的数组是否全为0
3、代码
所以其实可以认为,但凡涉及了真实数值和映射索引的,就是哈希编码了。
这里是对26个字符,映射到了0-25个索引值(因为a-z的ascii码是连续的,所以可以映射,'b'-'a'就是1,表示下标即可)。每个索引值对应在数组中可以存储a-z的出现次数。
比如这里的哈希函数,其实就是:s.charAt(i)-'a' ,即如果是字母b,计算出来是1,那么可以用这个索引来访问数组元素record[1],然后++即元素值+1,含义是字母数量+1.
class Solution {
public boolean isAnagram(String s, String t) {
int[] record = new int[26]; // 定义一个数组,长度为26
// 首先遍历s,对应字符位置+1
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i) - 'a']++; // s的第i个字符-'a'就是下标,该下标对应值+1
}
// 然后遍历t,对应字符位置-1
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i) - 'a']--;
}
for (int count: record) { // 增强型 for 循环 for (int count: record) 遍历 record 数组中的每个元素。变量 count 会依次取得数组中的每个值。
if (count != 0) { // 只有数组全为0,才证明s和t字符个数相同
return false;
}
}
return true;
}
}
可以看到其实这个数组长度为26,但是实际有值的只有几个,所以是牺牲了空间的。
4、复杂度分析
- 时间复杂度为O(n)
- 空间复杂度为O(1)。因为定义是的一个常量大小的辅助数组
三、两个数组的交集
1、题目:349. 两个数组的交集 - 力扣(LeetCode)
给定两个数组nums1和nums2,返回他们的共同元素,要去重,顺序无所谓。
2、思路
哈希表的最常见应用场景:给定一个数,判断是否出现过。
同样暴力解法的时间复杂度是O(n^2)。这里主要用set来实现哈希表(因为题目没有限制数组的长度、元素的大小,所以没法用数组了。万一是[1,2,100000]就不好开辟数组了)。
把nums1转化成哈希表,然后在nums2遍历,一个个在哈希表中查,在哈希表中就放入res。
unordered_set就可以直接去重,放入100个2,结果也只有1个2。
3、代码
1)数组无界限——用set实现
import java.util.HashSet;
import java.util.Set;
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0]; // 首先如果给定的数组是空的,那肯定返回空数组
}
Set<Integer> set1 = new HashSet<>();
Set<Integer> resSet = new HashSet<>(); // 定义两个hashset(天然可以去重)
//遍历数组1
for (int i : nums1) {
set1.add(i); // 把nums1中的所有元素添加到哈希表set1中(已去重)
}
//遍历数组2的过程中判断哈希表set1中是否存在该元素
for (int i : nums2) {
if (set1.contains(i)) {
resSet.add(i); // 如果nums2的元素在set1中有,就把这个数值添加到resSet集合中,这也直接是去重了的
}
}
//方法1:将结果集合转为数组
return resSet.stream().mapToInt(x -> x).toArray();
//方法2:另外申请一个数组存放setRes中的元素,最后返回数组
int[] arr = new int[resSet.size()];
int j = 0;
for(int i : resSet){
arr[j++] = i;
}
return arr;
}
}
2)数组有界限——用数组实现
力扣后面改了题目描述,增添了 数值范围,长度都在1000以内,数值为1-1000,就可以用数组了
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash1 = new int[1002]; // 长度都在1000以内了,其实长度定为1000就行
int[] hash2 = new int[1002];
for(int i : nums1) // 因为数值都在1-1000,所以映射可以一一对应
hash1[i]++; // hash1[i]表示数值i出现的次数
for(int i : nums2)
hash2[i]++;
List<Integer> resList = new ArrayList<>();
for(int i = 0; i < 1002; i++)
if(hash1[i] > 0 && hash2[i] > 0) // 所以有多少个不一样的数,就是多少个下标,不会重复
resList.add(i); // 如果在hash1和hash2对应下标数值都不为0,就把这个i放入列表
int index = 0;
int res[] = new int[resList.size()];
for(int i : resList)
res[index++] = i; // 从下标为0开始,放入列表中的所有值
return res;
}
}
- 直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。所以要是能用数组,尽量用数组。
4、复杂度分析
1)用set实现
- 时间复杂度: O(n + m) m 是最后要把 set转成vector
- 空间复杂度: O(n)
2)用数组实现
- 时间复杂度: O(m + n)
- 空间复杂度: O(n)
四、快乐数
1、题目:202. 快乐数 - 力扣(LeetCode)
对于数n,每次都计算它的平方和,直到最后如果等于1,就是快乐数。
2、思路
其实如果sum重复出现了,就会造成死循环。所以关键点是判断sum是否重复出现,如果重复出现了就是false,否则就一直找到sum=1为止。
所以就可以用哈希表,因为sum的值个数不确定,所以只能用set,这里用unordered_set。
3、代码
步骤1:用set存储每一个sum值,最初始为n,然后就是每一步的sum。
步骤2:每次都判断sum是否已经存在于set中。
步骤3:循环终止条件:1、n==1的时候,返回true;2、sum在set中,即n!=1,返回false。
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>(); // 定义一个哈希的set
while (n != 1 && !record.contains(n)) { // 如果最开始的n就在set中,那么直接结束
record.add(n); // 把最开始的n添加进去
n = getNextNumber(n); // 把n的平方和返回来。然后再进行是否在set中的判断
}
return n == 1; // 一直循环直到:1、如果n==1了,就返回true,2、如果sum在set中,那么n不为1,就返回false
}
private int getNextNumber(int n) { // 这是在做位数平方和的操作
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res; // 返回的是平方和
}
}
4、复杂度分析
- 时间复杂度: O(logn)
- 空间复杂度: O(logn)
五、两数之和
1、题目:1. 两数之和 - 力扣(LeetCode)
这是力扣的第一题。给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。(返回一组符合的就行)
2、思路
梦开始的地方,Leetcode:1.两数之和,学透哈希表,map使用有技巧!_哔哩哔哩_bilibili
暴力解法就是两层for循环,时间复杂度是O(n^2)。
- 不仅要知道元素值,还要知道下标,所以用map比较合适,key存元素值、value存下标
- 由于此题并不需要key是有序的,所以可以用std::unordered_map 效率更高
- 在遍历数组的时候,向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中。
- map存放的就是我们访问过的元素。
3、代码
遍历数组nums,每次遍历到nums[i]的时候,目标就是看target-nums[i]这个值是否在map中,如果在就是找到了,如果不在,就把nums[i]的值和下标存进map。
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2]; // 结果是两个下标,用数组返回
if(nums == null || nums.length == 0){
return res;
}
Map<Integer, Integer> map = new HashMap<>(); // 定义一个哈希map
for(int i = 0; i < nums.length; i++){ // 遍历nums数组
int temp = target - nums[i]; // 遍历nums[i]的时候,目标值是匹配到temp这个数
if(map.containsKey(temp)){ // 查看temp是否在map的key中
res[1] = i;
res[0] = map.get(temp); // 如果在map中,就返回下标get()这个方法
break;
}
map.put(nums[i], i); // 如果没找到,就把这个元素i和下标加入到map中
}
return res;
}
4、复杂度分析
- 时间复杂度: O(n) :因为从前向后遍历一遍nums就够了
- 空间复杂度: O(n)
六、四数相加2
1、题目:454. 四数相加 II - 力扣(LeetCode)
给4个相同长度的数组nums1-nums4,要分别取一个数使得和为0,求4个下标(i,j,k,l)有几种组合
2、思路
视频课:学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili
目标是找到:A[i] + B[j] + C[k] + D[l] = 0
暴力想法是4个for循环来遍历,O(n^4)的时间复杂度。所以我们向把问题一拆为二,仿照两个数组之和的方式来做(先把一个数组的元素放入哈希表,再用另一个数组遍历比对)
因为sum不可控,所以用数组不行。同时因为要统计第一半问题中A+B元素和的组成数量,所以不仅要统计是否出现过,还要统计出现的次数,所以可以想到key-value,用map最为合适。同样用unordered-map来做。
步骤1:遍历A和B两个数组,key放元素和、value放和出现的次数,存入map
步骤2:遍历C和D两个数组,每次和是c+d,所以目标就是在map中找是否存在0-(c+d)。
步骤3:用一个count记录,如果每次在map中找到了,就count++
3、代码
为什么要用map,为什么要统计sum出现的次数?
因为目标是求4元组可能的个数。所以对于AB两个数组,求出每个sum的数量,就是每个sum可能的组成方式有几种。然后再用CD的和一一比对,比对上了就res加上sum的组成个数。
因为在遍历CD的时候,每一轮都会直接在res上面加,所以不用再额外统计次数了。但是AB要统计有几种sum的组成方法。
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int res = 0; // 记录满足的次数
Map<Integer, Integer> map = new HashMap<Integer, Integer>(); // 定义一个哈希map
//首先遍历num1和num2,对每个元素一一求和,同时统计出现的次数,放入map
for (int i : nums1) { // 这个遍历方法是取出所有值
for (int j : nums2) {
int sum = i + j;
map.put(sum, map.getOrDefault(sum, 0) + 1); // key是sum,value是次数
}
}
//然后遍历剩下的俩个数组nums3和nums4
for (int i : nums3) { // 同样计算两个元素的和
for (int j : nums4) { // ij是数组中的所有值
res += map.getOrDefault(0 - i - j, 0); // 如果0-(i+j)存在就返回对应的value值,res+前面sum的个数
}
}
return res;
}
}
4、复杂度分析
- 时间复杂度: O(n^2) 其实是2*n^2
- 空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2
七、赎金信(数组哈希)
1、题目:383. 赎金信 - 力扣(LeetCode)
“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思”
给两个字符串ransomNote和magazine,判断ransomNote能不能由magazine里的字符构成(B中每个字符只能在A中用一次)说明了只有小写字母。ransomNote="aa", magazine="aab" 返回true
2、思路
和字母异位词有点相似,相当于求字符串a和字符串是否可以相互组成。而且这个题还是单边的
暴力解法:2层for循环,外层是magazine的遍历,内层是ransomNote的遍历,如果遇到相同的字母了,就从ransomNote中删除这个字符,最后如果ransomNote为空了,就为true。时间复杂度: O(n^2)
哈希解法:因为说了只有小写字符,所以可以用数组做哈希表。(如果用map的话要用红黑树,因为magazine中的字符可以是重复的)
用一个长度为26的数组来记录magazine里字母出现的次数。然后再遍历ransomNote,去验证数组是否包含了ransomNote所需要的所有字母。
3、代码
步骤1:定义一个长度为26的数组,定位索引到a-z每个字母。
步骤2:先遍历magazine,如果遇到了字母,就把对应位置+1。
步骤3:然后遍历ransomNote,遇到了字母,就把对应位置-1。
步骤4:最后检查数组,如果存在负数,就证明ransomNote中出现了magazine中没有的字符。
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false; // 必须magazine更长才行
}
int[] record = new int[26]; // 定义一个长度为26的哈希映射数组
// 遍历magazine字符串
for(char c : magazine.toCharArray()){
record[c - 'a'] += 1; // 下标可以定位到每个字符,值为该字符的个数
}
// 然后遍历ransomNote字符串
for(char c : ransomNote.toCharArray()){
record[c - 'a'] -= 1; // 把每个字符对应位置的数-1
}
// 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符
for(int i : record){
if(i < 0){
return false;
}
}
return true;
}
}
4、复杂度分析
- 时间复杂度: O(n)
- 空间复杂度: O(1)
八、三数之和
1、题目:15. 三数之和 - 力扣(LeetCode)
给一个整数数组,判断里面是否能找出3个数,相加为0。返回所有可能的结果。
比如输入nums=[-1,0,1,2,-1,-4],输出[ [-1,-1,2] , [-1,0,1] ]
相对于两数之和,就复杂在还要去重。
2、思路
视频课:难在去重和剪枝!| LeetCode:18. 四数之和_哔哩哔哩_bilibili
1)哈希解法
就是现用两层for循环,把a+b的数组存储在哈希表里面,然后再遍历数组,查找0-(a+b)。
但是问题是不能存在重复的三元组。除非把所有可能的结果找出来再去重,效率比较低。
2)双指针解法
其实这道题用哈希法并不十分合适,因为要去重的话会比较麻烦。优先用双指针法。
如下面这个动图,相当于三个元素就是nums[i]、nums[left]、nums[right]
- 步骤1:对nums进行从小到大排序。最外层循环是i,从0开始向右。left初始为0,向右移动,right指针初始在最右边,向左移动。
- 步骤2:对于每一轮,i值是固定的。如果三元组之和>0,就要让right指针左移,让总和小一点。如果三元组之和<0,就让left指针右移,才能让三数之和大一点。
- 步骤3:每一轮次下,都直到left和right相遇,就跳出内部循环,让i右移。
去重逻辑非常重要:abc三个元素都要去重。
要求不能是重复的三元组,但是三元组内部可以是重复的,{-1,-1,2}是可以的
1、对a怎么去重? 判断nums[i]和nums[i-1]相等就跳过。比如说如果nums是{-1,-1,2}如果判断i和它后面一个元素,那么这个结果就不会返回。所以要判断i和它前面一个数。比如说遍历到第二个-1,判断后就直接跳过,因为对于i是-1的情况已经全部循环过一轮了,如果再循环,就回可能返回重复的结果。
2、b和c的去重:比如说nums=[0,-1,-1,-1,-1,1,1,1,1,]。那么如果已经收获了[0,-1,1],判断left如果跟left+1相同,就跳过这个left+1,把left往右边移动。right和right-1相同,就持续把right--往中间移动。(因为如果sum=0的话,本来就要右移left左移right,所以加上去重移动的,就移动了2步)
3、代码
1)哈希解法
可以看到,去重的操作确实比较麻烦。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>(); // 用这个数组列表,来存放所有符合的三元组
Arrays.sort(nums); // 把nums从小到大排序
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) { // 如果三元组的第一个元素大于零,不可能凑成三元组
return result;
}
// 三元组元素a去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue; // 如果当前数和上一个数相同,也跳过(因为三元组需要去重)
}
HashSet<Integer> set = new HashSet<>();
for (int j = i + 1; j < nums.length; j++) { // 遍历i后面的所有元素
// 三元组元素b去重
if (j > i + 2 && nums[j] == nums[j - 1] && nums[j - 1] == nums[j - 2]) {
continue; // b元素需要保证不和其前一个或者前两个相同
}
int c = -nums[i] - nums[j]; // 目标需要找的值是c
if (set.contains(c)) {
result.add(Arrays.asList(nums[i], nums[j], c)); // 把三个元素放入三元组
set.remove(c); // 三元组元素c去重,要保证三元组不重复
} else {
set.add(nums[j]); // 把j对应的元素放入set
}
}
}
return result;
}
}
2)双指针解法
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>(); // 相当于一个二元数组
Arrays.sort(nums); // 同样先对数组进行排序
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) { // 因为已经排序,所以如果第一个元素大于0,那肯定组不成三元组了,直接return
return result;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 去重a
continue; // 如果当前的i和上一个i值相同,直接不用去看了
}
int left = i + 1; // left永远在i的下一个 开始
int right = nums.length - 1; // right永远从最右边开始
while (right > left) { // 一旦left和right相遇就终止
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) { // 如果三数之和大于0就right左移
right--;
} else if (sum < 0) { // 如果三数之和小于0就left右移
left++;
} else { // 如果等于0 ,就把这个结果放进元组列表
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 接着移动right和left进行寻找
right--;
left++; // 如果找到了,就同时向中间移动left和right。
// 所以如果还去重了的话,相当于移动了两步
}
}
}
return result;
}
}
4、复杂度分析
哈希法的时间复杂度是 :O(n^2),空间复杂度是: O(n)。
双指针法的时间复杂度:O(n^2)。空间复杂度是: O(1),因为不用额外建哈希表了。
九、四数之和
1、题目:18. 四数之和 - 力扣(LeetCode)
给一个nums数组,给一个目标值target,按照三数之和逻辑,返回不重复的所有可能四元组。
比如nums=[1,0,-1,0,-2,2],target=0,返回 [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]
2、思路
视频课:难在去重和剪枝!| LeetCode:18. 四数之和_哔哩哔哩_bilibili
和三数之和是一个思路,就是再套一层for循环就行。
三数之和最开始是比较nums[i]如果大于0就直接退出。但是这里要增加判断:
- 步骤一:这其实就是所谓的剪枝。排序之后如果nums[i] > target 并且nums[i] >=0 。这里要规避数组元素为负数的情况。防止出现
[-4, -3, -2, -1]
,target
是-10的情况。
- 步骤二:三数之和是每轮固定i,但是这里需要用两层for循环nums[k] + nums[i]为确定值。然后同样的方法,在内层移动left和right就行。
- 步骤三:找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况。
一样的道理,五数之和、六数之和等等都采用这种解法
3、代码
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums); // 同样先对数组进行排序
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0 && nums[i] > target) { // 如果大于target并且为正
return result; // 就不会出现四元组了,直接返回
}
if (i > 0 && nums[i - 1] == nums[i]) { // 对nums[i]去重
continue; // 判断和它的前一个值如果相等,直接跳过
}
for (int j = i + 1; j < nums.length; j++) { // 对第二个元素
if (nums[i]+nums[j] > 0 && nums[i]+nums[j] > target) {
return result; // 逻辑一样,如果两个值就大于target了就直接返回,但是也要注意考虑负数的情况
}
if (j > i + 1 && nums[j - 1] == nums[j]) { // 对nums[j]去重
continue;
}
int left = j + 1; // left是j的下一个,right是末尾开始
int right = nums.length - 1;
while (right > left) {
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) { //和三数之和一样
right--;
} else if (sum < target) {
left++;
} else { // 找到四元组,写入
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
// 对nums[left]和nums[right]去重(和三元组一样的逻辑)
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
left++;
right--;
}
}
}
}
return result;
}
}
4、复杂度分析
- 时间复杂度:三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。(将原本暴力O(n^4)的解法,降为O(n^3)的解法。)
- 空间复杂度:O(1)