前几天遇上了一块硬骨头,看了题目半天。根本不知道怎么写,去看了题解又死活想不通为什么要用单调栈,或者说单调栈凭什么能保证出来的数字是最大的?原题链接https://leetcode-cn.com/problems/create-maximum-number/
直到昨天我才发现了原来我一直理解错题目了,本题的意思是最后返回的数组组成的数字最大,而不是数组的和最大。我之前一直理解成数组和最大,认为[6,7,3,2,4]最后形成的单调栈为[7,4]按照我原先的理解“和最大”,我认为[6,7]的和明显比[7,4]要大。直到我看懂了题目,那没事了。
首先说一下这个题目的解法,我们分为以下两步:
1 分别在两个数组里面找出最大的几项
2 把这两个数组进行合并
其实难的地方就在第一步,因为第二步的过程与归并排序的归并过程十分类似。那么第一步到底该如何处理?其实使用单调栈就可以完美地解决这个问题。
按照题目的意思,我们在数组中肯定要找出一个最大的数,后面跟着比他小的数,这是因为在长度相同的两个数中,以8开头的数是绝对比以7开头的数要大的,无论7后面的数字是怎样天花乱坠,如果两个数开头都是8的话,那么就继续比较后面的数字大小。但是你不能对这个数组进行排序,因为必须保持数字其在原数组中的相对顺序,那么为什么单调栈就能实现这个功能呢?
首先说明一下啥是单调栈,因为我在做这道题之前也没了解过这个功能特异的数据结构。其实他的底层实现就是一个普通的栈,只不过就是栈里面的数据是单调递增或者单调递减的。例如[3,5,7,8]和[6,3,1]都是一个单调栈。本道题中我们需要实现的单调栈是从栈顶到栈底单调递增的。我们拿实例1中的数据来说:nums2 = [9, 1, 2, 5, 8, 3]
1)栈空,对于9来说,栈是空的,我们就让其入栈。
2) 1<9,所以1入栈,此时栈元素是 9 ,1
3) 2>1,1出栈,2入栈,此时栈内元素是9,2
4) 5>2,2出栈,5入栈,此时栈内元素是9,5
5) 8>5,5出栈,8入栈,此时栈内元素是9,8
6) 3<8,3入栈,此时栈内元素是9,8,3
可以看出来,使用单调栈时,不但能保证出来的数是最大的,还能维持这些数的相对顺序。
我先将我AC的代码发出来
class Solution {
public:
vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
int m = nums1.size(), n = nums2.size();
vector<int> ret(k, 0);
int begin= max(0,k-n), end = min(k,m);
for (int i=begin;i<=end;i++){
vector<int> stack1(dandiaostack(nums1,i));
vector<int> stack2(dandiaostack(nums2,k-i));
vector<int> cur(merge(stack1,stack2));
if(compare(cur,0,ret,0)>0)
ret.swap(cur);
}
return ret;
}
vector<int> dandiaostack(vector<int>& nums,int length){
int drop=nums.size()-length;
stack<int> s;
for(int i=0;i<nums.size();i++){
while(!s.empty()&&s.top()<nums[i]&&drop>0){
s.pop();
drop--;
}
if(s.size()<length)
s.push(nums[i]);
else
drop--;
}
vector<int> ret;
while(!s.empty()){
ret.insert(ret.begin(),s.top());
s.pop();
}
return ret;
}
int compare(vector<int>& nums1,int index1,vector<int>& nums2,int index2){
while(index1<nums1.size()&&index2<nums2.size()){
if(nums1[index1]!=nums2[index2])
return nums1[index1]-nums2[index2];
index1++;
index2++;
}
return (nums1.size()-index1)-(nums2.size()-index2);
}
vector<int> merge(vector<int>& nums1,vector<int>& nums2){
if(!nums1.size())
return nums2;
if(!nums2.size())
return nums1;
int a=0,b=0,i=0;
int sum=nums1.size()+nums2.size();
vector<int> ret(sum,0);
while(i<sum){
if(compare(nums1,a,nums2,b)>0)
ret[i++]=nums1[a++];
else
ret[i++]=nums2[b++];
}
return ret;
}
};
看起来好像代码很长,说实话,当我看别人代码这么零零散散一大堆的时候,我也懒得看。不过我相信我下面的讲解可以让这段代码变得容易理解。
这个代码分为四块,其中一个是main函数(并不是严格的main函数),另外三个一个作用是构造单调栈(就是我们提到的第一步),一个是归并的过程(就是我们提到的第二部),还有一个是compare辅助函数,无论是在归并过程或者是main函数中都让我们方便了很多。
首先我想说的是在main函数中如何能找到最大的值,我们可以使用枚举算法, 既然需要选出k个数字,那么我们可以依次枚举nums1中取0个数 取1个数 ……一直到取k个数,那么相对应地,nums2就依次取k,k-1……0个数。不过细心的同学可以发现,为什么我的main函数中左边界是max(0,k-n) 而右边界是min(k,m),这其实不难理解,因为如果nums1中只有3个数,而此时的k需要5个数,我们取值范围只能从0-3而不能到5;于此同时,如果nums2中只有3个数,k还是需要5个数,那么nums1至少需要取2个数,这个时候取值范围就变成了2-3。这样子做可以防止下标越界同时也算是一次剪枝。
其次关于我写的单调栈这个函数,这道题的单调栈和我上面说的普通单调栈还有一点区别,这是因为当我们需要从nums1数组中取5个数时,若nums1是[5,7,6,3,8,3,1],平常的结果应是 8,3,1。但是现在的情况我们需要取5个数,所以我设置了一个drop变量,目的就是算一下我们还能再扔掉几个数。就我的这个例子来看,drop=nums.size()-k=2,也就是说我们只能扔掉两个数,后面的话不管你愿不愿意,都必须组成这个特殊的单调栈。
1)栈空,5入栈,栈内元素为5
2)7>5, 5出栈,7入栈,栈内元素为7,此时drop为1
3)6<7,6入栈,栈内元素为 7 6
4)3<6,3入栈,栈内元素为 7 6 3
5)8>3,3出栈,栈内元素为7 6,此时drop为0,已经不能再出栈了
6)后面的元素全部加入,栈内元素为 7 6 8 3 1
相信此时再看我的单调栈函数已经不再困难了吧 :)
然后我再说说这个compare辅助函数,这个函数很简单,就是实现两个数组的比较,如果nums1的index下标的元素比nums2的index下标的第一个元素不相等,那么就可以直接返回nums1[index1]-nums2[index2];返回值大于0就是nums1[index1]大,否则就是nums2[index2]大。不相等时就依次比较下面的数,如果都不满足的话,就return (nums1.size()-index1)-(nums2.size()-index2);有两种情况会触发这句话
1)nums1或者nums2中的一个index已经越界了,那么就肯定取另一个
2)nums1和nums2前面的n个元素全相等,其中一个数组长度只有n,例如nums1[5,5,5,7]和nums2[5,5,5]依次比较时,我们就取长的,因为
当我们取完5的时候,nums2变成[7],此时7>5,这样子才是最优解,当然这个数字也可能小于5,不过我们这样做就可以保证一定是最优解。
最后就说这个归并过程,两个数组nums1[5,4,2]和nums2[6,3,1]
a和b都为0,也就是nums1和nums2开始的下标
1)6>5 ret数组加入6, b++
2) 5>3 ret数组为[6,5] a++
3) 4>3 ret数组为[6,5,4] a++
4) 3>2 ret数组为[6,5,4,3] b++
5)2>1 ret数组为[6,5,4,3,2,1] a++
6)nums1中的下标已经越界了,ret数组为[6,5,4,3,2,1] b++
这时候出来的数组就一定是最大的
其实我之前一直在思考为什么这两个比较的时候一定需要用到compare函数,既然两个数组都是单调的,例如 [5,3,2]和[5,2,1]我们无论从哪个数组里拿出5,下一次拿的值也一定是5,因为后面的数是一定小于等于前面的数。但是我想到了两个数组不单调! 因为这是一个特殊的单调栈,我前面已经说过了。就拿那个例子来说:7 6 8 3 1,他就不是一个单调递减的数组,例如它和[9,6,3,1]进行merge操作,那么在ret数组里为[9,7]的时候,两个数组的index上的元素都为6,就应该先取走后面的数比较大的那个,会形成[9,7,6,8],可以明显发现9768比9766要大的。
相信我说了这么多,应该就会觉得这道题已经不是毫无头绪了吧,我觉得写代码就应该亲手敲,纵使你是照猫画虎效果也是比你光看好多了!继续加油:)