算法小结(数组与字符串)

c++中对应的容器

1、普通的数组
对于数值型数组,包括int、long、float、double
对于字符串,可以用字符型数组
2、STL中的vector

3、STL中的string
关于容量的方法:size与length等价;max_size返回字符串的最大长度,与系统有关;capacity返回数组容量,string应该是用动态数组来实现,这个容量就是当前数组的最大容量;resize方法改变的是length而不是capacity;empty
关于读取的方法:重载符号[],at,back,front
关于清空的方法:clear
关于修改的方法:重载符号+和=,append,push_back,pop_back,insert,erase,replace,swap,assign
特殊方法:copy,find,substr(截取一段),compare(其实关系运算符也都有重载)

容器在使用中需要注意的问题

1、数组的初始化
2、数值过大时可以用字符型数组来存储
3、vector的创建,元素的插入与删除
4、vector与list的区别
5、string的可用操作
6、字符数组的末位’\0’是默认会自己添上的
7、vector的vector

算法中需要注意的问题

1、二分法的终止条件与初始化问题
二分法的模板在leetcode上 https://leetcode-cn.com/explore/learn/card/binary-search/209/template-i/835/ 已经有很好地总结,这里结合它说一下自己的理解。
模板一:用在只需要关注当前元素的情况下,不需要结合左右相邻元素来判断是否满足条件或者确定搜索方向;不需要做后处理,因为每一步都在检查是否找到了目标,如果完整走完了循环,说明没有找到。
向左向右查询时都不要再考虑mid,因为mid已经不满足条件,所以下一步的边界是mid+1或者-1

初始条件:left = 0, right = length-1
终止:left > right
向左查找:right = mid-1
向右查找:left = mid+1

模板二:用在需要根据当前元素和右邻来确定是否满足条件或确定搜索方向时;在整个循环中需要保证至少存在两个元素,如果只剩下一个元素了,也即left=right了,需要退出循环,并且做后处理判断这个剩余元素是否满足条件。
注意这里right的初始条件变成了length(原因是什么?似乎绝大多数情况都是length-1更合适,至少不会越界)
以及这里为什么是right=mid以及left=mid+1:
1、我们关注的是当前元素mid和它的右邻mid+1,做出的判断是mid这个元素是否满足条件,mid+1是用来辅助判断的,但却是不可缺少。
2、那么当mid不满足条件,依据什么来判断在下一步的搜寻中是否还可能要用到mid这个元素,就是这个元素是否会成为别人的右邻。
3、如果下一步是向右查找,那么原本在mid左边的元素全都不用考虑了,那么mid自然无法成为接下来搜寻范围内任何元素的右邻了,mid也就不用囊括在接下来的搜寻范围内,因此mid+1。
4、如果接下来是向左查找,mid的左边的元素还在,它是有可能成为别人的右邻的,虽然自己不可能是target了,但是能作为辅助去判断它前一个元素是否target,所以它不能被排除在外,right=mid
5、最后剩下来的元素的下标是left=right
6、邻居的定义有时候不止是相差一位,可以是很多位

初始条件:left = 0, right = length
终止:left == right
向左查找:right = mid
向右查找:left = mid+1

模板2的应用例题是剑指offer-53:在排序数组中查找数字,题中需要去找连续相等序列的右边界,需要关注当前元素和右邻,这正好是模板2;同时也需要去找连续相等序列的左边界,需要关注当前元素和左邻,本质上和模板没有区别,只是这个时候向左查找时right=mid-1,向右查找时left=mid

还有一道leetcode上很有意思的例题162寻找峰值

峰值元素是指其值大于左右相邻值的元素。 给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。 你可以假设 nums[-1] = nums[n] = -∞

简略审题好像左右邻都需要用到,但是其实只需要用到右邻,由于题目说可以假设nums[-1] = nums[n] = -∞,因此只要能找到上坡,就必定会有顶峰,因为有最左最左的负无穷作为保障。因此

规律一:如果nums[i] > nums[i+1],则在i之前一定存在峰值元素
规律二:如果nums[i] < nums[i+1],则在i+1之后一定存在峰值元素

只需要右邻,套模板二即可。

模板3:模板3是用在需要关注当前元素同时关注左右邻的情况,用了前面模板2的分析,模板3就很好理解了,只是模板2的进阶版,由于要确保搜索范围内至少有三个元素,所以边界条件就是left+1==right,即只剩下两个元素时;因为需要关注左邻和右邻,所以right=mid,left=mid;完整退出循环时还剩下left和right两个元素未经过判断,所以对这两个元素做后处理,谁满足条件就输出谁

初始条件:left = 0, right = length-1
终止:left + 1 == right
向左查找:right = mid
向右查找:left = mid

题目汇总

1、查找

查找类题目是在数组中寻找有特殊性质的一个或多个数字。这类题目有两个特点,1、要么是要寻找的数字特别,数组本身是乱序无范围的,2、要么寻找的数字没有特点,是任意一个数字,但是这个数组本身具有特殊性质。当然不排除要查找的数字和数组都具有特殊性质,但是两者本身都没有特点的题目还未见过。
3、4、11、39、41、50、53。

通用法则

1、暴力搜索,这种方法时间效率都不高
2、哈希表,以空间换时间,可以用STL中的unordered_map或者是用红黑树的map,但在字符查找时,通常用一个256空间的整型数组就可以实现哈希表的功能。在不需要有序映射的时候,用unordered_map拥有更高效率。
3、为乱序数组排序,通常能降低难度,但是也会占用一定的时间消耗,排序可以直接调用algorithm中的sort方法。排序可以帮助缩小搜索范围,可以使得重复数字相连

在特殊数组中寻找数字

剑指offer-3:数组中的重复数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
分析:数组的特点在于长度为n,且数字均在0~n-1的范围内,那么每个数字总会找到跟自己本身相等的数组index,利用这个特点去遍历数组,把每个数字放到自己对应的index,重复数字一定会发现自己的“坑”被占了。这实现了O(1)的空间复杂度,O(N)的时间复杂度。这道题用排序、哈希表、暴力搜索这三个通用法则依然是可以解决的,但是效率比不上利用数组特点的方法。

剑指offer-4:二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
分析:该数组的特点在于二维以及排序,二维本身并不能提供太多的帮助,只会增加难度;回想一维排序数组查找某个数字最高效的应该是二分法,但是用到这里不太恰当,因为这个数组只是局部有序。但是按照二分法的思想,是要逐步缩小搜小范围,用在这里也合适。缩小范围要考虑通过某个准则,不断确定下一轮的搜索方向,利用排序数组的特点,可以通过比较大小来确定。又考虑到时二维数组,搜索的维度是二维,那么就需要衡量比较大小时,横向和纵向应具有不同的特点,这样才能具有确定的搜索方向。考虑从右上角向左下角搜索,或者相反,这是水平方向是递减的,垂直方向是递增的,这样搜索位置如果比给定数字要大,只会往水平方向搜索,如果小则往垂直方向。但是如果考虑从左上到右下搜索,会出现往水平和往垂直搜索都可以的情况,这也就没办法确定下一步方向了。总的思路就是:排序>>缩小范围地搜索>>二维(要考虑搜索起始点和搜索方向)

剑指offer-11:旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
分析:数组特点:旋转,部分有序。可以从头遍历,复杂度为O(N)。由于依然是部分有序的,二分法还是有用武之地,只要确定搜索方向的准则即可。这道题的坑在于非严格递增,即存在重复数字,如果发现mid和left以及right三者相等,就只能顺序遍历

剑指offer-39:数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
分析:这题是所要寻找的数字本身具有特点——出现次数超过一半。用排序、哈希表自然可以解决,排序时间复杂度为nlogn,哈希表可以达到O(n),但是同时需要O(n)的空间。数组本身没有特点,要从查找的数字本身寻找特点,数字出现的次数超过了一半,则它一定是数组的中位数。所以可以转换为寻找中位数。所谓中位数就是在排序数组中位列中间的数组,所以可以用Partition函数找到位列第k位的数字,时间复杂度为O(N)。从这道题也可以拓展,要找到第k大的数字同样可以用partition函数。
剑指offer提供了第二种思路,在遍历数组时保存两个变量,一个是数组中的一个数x,另一个是出现次数n。遍历判断当前数与x是否相等,如果是则n++,否则n–,若n减到0,则将x更换成当前数,n重设为1。那么要找的数比其他数字出现的次数加起来还要多一次,所以最后保存下来的x就是要找的数。

//Partition函数使得数组的其中一个数字,左边全是比它小的数字,右边全是比它大的数字;也即是说把这个数字放在了排序后应该处于的正确位置,函数返回值是它最终的位置,因此这个函数也可以用来找第k大的数字。但是完成后左右不一定有序。
//在实际实现中,因为Partition函数并没有特别要求这一轮要针对数组中的哪一个数字,可以在给定范围内任选。
//下面的实现是总是选择给定范围最左边的数字x,然后将比它小所有数字依次排列在它后面(注意,就是找比它小的),用index去记录,遍历完之后,再将x和index-1位置上的数做交换,即可完成排列,最后范围index-1。
//这个特别慢
int Partition(vector<int>& numbers, int left, int right){
	int pivot=left;
	int index = pivot+1;
	for(int i=index;i<=right;++i){
		if(numbers[i]<numbers[pivot]){
			swap(numbers,i,index);
			index++;
		}
	}
	sawp(numbers,pivot,index-1);
	return index-1;
}
//这个快得多,但是还没看懂
int partation(vector<int>& nums, int low, int high) {
	int left = low + 1, right = high;
	swap(nums[low], nums[(low + high) / 2]);
	int bound = nums[low];
	//  双指针,快速排序,交换不符合条件的数据
	while (left <= right) {
	    while (left < high && nums[left] >= bound) left++;
	    while (nums[right] < bound) right--;
	    if (left < right) 
	        swap(nums[left++], nums[right--]);
	    else break;
	}
	//  将bound放到换分完成后的两个数组之间,作为边界, 返回bound的位次
	swap(nums[low], nums[right]);
	return right;
}

leecode-961:重复N次的数字
在大小为 2N 的数组 A 中有 N+1 个不同的元素,其中有一个元素重复了 N 次。返回重复了 N 次的那个元素。
分析:这题也是在重复超过半数的数字,但是这题本身的数组有特点。总共2N个数,但是只有有N+1个不同的元素,说明除了那个重复元素的以外,其他元素都只出现了一次。因此只要找到一对重复的元素即可,用随机搜索即可,不过时间不稳定。当然这题用Partition和哈希表或者排序,甚至清奇的计数思路也依旧可行,因为依然是在找中位数,只不过没有利用到数组本身的特点。

剑指offer-41:数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
分析:本题的特点是数据流,数据流内的数字没有其他特点。前面两道题都是把出现次数超过一半转换为求中位数,但是需要明确的是,一般的中位数并不保证出现次数超过一半,这种与计数无关的查找类题目通常就用不上哈希表了。但是partition方法和排序方法依然是可行的。
数据流既要考虑插入复杂度,也要考虑数据处理复杂度。partition方法对数组没有要求,插入可以直接进行,O(1),找中位数需要O(n)的复杂度。排序方法是去维护一个排序数组或者排序链表,那么插入就需要O(n)的复杂度,但是找中位数只需要O(1)的复杂度。
剑指offer提供了其他思路:
1、二分搜索树,维护二分搜索树只需要O(logn)的复杂度,相比于排序数组或者排序链表快点,但是当二分搜索树极度不平衡时会退化成排序链表。如要在在二分搜索树中寻找中位数,需要在节点结构新添一个变量index,在插入元素时,从根节点开始遍历,如果当前节点不是插入位置,则一定会插入到它的子树中,因此增加一个步骤index++。在寻找中位数时,遍历节点,如果左子树的节点数等于总结点数的一半,那么该节点就是中位数,如果比总结点数的一半大,那就往左子树遍历,否则往右子树遍历。
2、AVL树,AVL是为了避免二叉搜索树退化成链表而设置的一种数据结构,AVL树任意一个节点的左右子树的高度差(平衡因子)不会超过1,它本质是依旧是二叉搜索树,只不过有了平衡因子的限制,在插入元素时,如果平衡因子超过1,需要进行左旋转或者右旋转的操作。这道题将平衡因子修改为左右子树节点数目之差,就可以以O(1)的复杂度获取中位数,插入的复杂度仍然是O(logn)。(左旋和右旋怎么做?)
3、最大堆最小堆,考虑一个排序数组,中位数应该处于数组的中间位置,如果是数组容量是偶数,那么就是数组中间两个数的平均。其实只需要维护好这两个位置即可,前面和后面的数字是否有序并没有关系。所以问题就转化为维护一个最大堆和一个最小堆的问题。中位数由最大堆的堆顶和最小堆的堆顶求出来,如果两者容量之和是偶数,那就取两个堆顶的平均数,如果是奇数,就取其中一个,具体取哪一个,是由自己制定的入堆规则决定的。这样插入元素的复杂度为O(logn)(慕课网堆中的上浮下潜操作),获取中位数只需要O(1)的复杂度。
最大堆最小堆可以用STL中的函数push_heap,pop_heap,vector来实现

//最大堆
vector<int> max;
max.push_back(num); //插入元素
push_heap(max.begin(),max.end(),less<int>()); //插入元素后需要调整维护最大根

pop_heap(max.begin(),max.end(),less<int>()); //弹出元素时需要先将pop_heap调整顺序
max.pop_back(); //再将末位弹出

//最小堆
vector<int> min;
min.push_back(num);
push_heap(min.begin(),min.end(),greater<int>()); //维护最小堆时是 用greater()

pop_heap(min.begin(),min.end(),greater<int>());
min.pop_back();

struct TreeNode{
	int val;
	int index; //表示子树的节点数目
	TreeNode* left;
	TreeNode* right;
}

剑指offer-50:第一个只出现一次的字符
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)
分析:字符型的查找问题,排序可能不是一个好的选择,哈希表仍旧可以用,而且在字符型的哈希表中不需要用到STL中map或者unordered_map,直接定义一个容量为256的数组即可。

unsigned int hashTable[256] = {0}; //用于计数,用unsigned int
for(int i=0;i<length;++i){
    hashTable[str[i]]++;
}

剑指offer-53:在排序数组中查找数字
统计一个数字在排序数组中出现的次数。
分析:由于是排序数组,从头开始遍历也是一种方法,时间复杂度为O(n),二分法也是一种方法。但是二分法在这道题也有不一样的应用方法,一种是用二分法是找到其中一个数字的位置,然后向左向右遍历找到起始点和终止点,二分法复杂度为O(logn),但是这里仍然是用了遍历,复杂度仍是O(n),因此这和从头遍历并没有区别。其实只要用明确了起始点和终止点的特点,用二分法直接去找这两个点即可。
用二分法需要有两个条件,一是可终止,而是可确定搜索方向。可终止是指要寻找的数需要具备特性,判断当前数是否具备这个特性从而终止二分;可确定搜索方向是当前数不是要找的数时,可以根据这个数本身确定下一步应该往哪个方向继续搜寻,这也是为什么排序数组中通常可以用上二分法,有序可以帮助我们确定下一步方向。
回到这道题,起始点的特点是左边不等于这个数,终止点的特点是右边不等于这个数,但是起始点和终止点上等于这个数。至于搜索方向,跟第一种二分法是一样的,当前数如果小于所要寻找到的数,往右边继续找,否则往左边。所以跟第一种二分法相比,仅仅是在终止条件中多了一个限制。当然这里由于要用到旁边的数来进行判断,还需要特别注意越界问题,在寻找左边界时,如果当前数已经没有左边的数,也即是第一个数,那它自然也可以成为左边界,右边界同理。

小结
查找类题目有两个通法,如果数组有序往往可以降低难度,可以应用上二分法或者排序本身带来的性质。哈希表也是个通法,在与数字出现次数直接或间接相关的查找类题目中,哈希表通常拥有线性的时间复杂度,但是牺牲了空间复杂度。在面试中若一开始没有特别好的思路,可以先说说排序或者哈希表这两个方法,暴力搜索当然也是一种方法,不过没有必要提及了。

找中位数:排序、partition,最大堆最小堆,如果中位数是次数超过一半的数字,还可以用哈希表或者计数方法。

2、排序

剑指offer-40:最小的k个数
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
分析:要找最小的k个数,可以看做是一种局部排序。既然如此就很容易获得两种思路,一种是用STL的sort方法做整个数组的排序,复杂度为O(nlogn),然后输出前k个数;第二种是不断使用Partition方法,直到partition的输出是等于k-1,证明最小的k个数已经放在数组的前k个位置,然后输出这前k个数即可,注意这前k个数不一定是有序地输出出来。Partition是O(n)的方法,但是需要调用多次,这里的复杂不知道怎么算出来的,书上只说整个流程是O(n),为什么?

vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        int length = input.size();
        vector<int> res;
        //这里的k还需要排除等于0的情况,否则会报错超时,竟然debug了半天是这个问题。。
        if(length<k || k<=0 || length<=0)
            return res;
        int left = 0;
        int right = length-1;
        int index = Partition(input,left,right);
        while(index!=k-1){
            if(index>k-1){
                right = index-1;
                index = Partition(input,left,right);
            }
            else{
                left = index+1;
                index = Partition(input,left,right);
            }
        }
        
        //res.assign(input.begin(),input.begin()+k-1);
        for(int i=0;i<k;++i)
            res.push_back(input[i]);
        return res;        
    }

本题提供一种解法,这种解法有它的适用场景,设想数据是海量,数组没法一次性载入到内存中,那么就需要从辅助空间中逐步读入。所以在n很大k很小的时候可以用,难以避免的是它的时间复杂度会相对于前面的partition方法高。
具体来说,用一个大小为k的容器去装载数组中最小的k个数。从数组中一个一个地读取数字,如果容器未满,则将数字填入,如果容器已满,则要判断这个数字是否比容器中的最大数还大,如果是则舍弃,如果不是,则插入到容器内,并把容器中的最大数弹出。
现在主要问题就是这个容器应用采用什么数据结构。一是最大堆,而是红黑树。
可以发现这个容器最好具有排序的特点,且容器不断需要插入弹出,用二分搜索树能够具有较高综合性能。又考虑到其实我们一直都是只关心容器中的最大数,所以采用最大堆就足矣,它能以O(1)的复杂度取出最大数,插入与取出维持二分搜索树的复杂度O(logn)。不过堆是用动态数组实现的,它要求连续的数据存储结构,在本题中,k一般是较小的数,所以没有关系,但如果k很大且资源紧张,要从内存中获取一段很长的连续空间是不太现实的。最大堆可以用STL中的vector配合push_heap实现,也可以直接用STL中的优先队列priority_queue实现,因为它的底层实现正是堆。
另一种选择是用红黑树,红黑树可以实现插入、查找、删除的复杂度都是O(logn),且不要求连续的存储空间。STL中的set、multiset、map、multimap都是用这种数据结构(liyubobo:set和map是可以互为底层的。所以set和map其实可以看做是一个东西),在定义时加上greater和less就可以分别实现一个降序、升序的数据结构。

附上STL各种数据结构的底层实现,参考资料https://www.cnblogs.com/hustlijian/p/3611424.html,hash_xxx现在都更名成unordered_xxx,无序的map和set的底层正是哈希表,所以以前也叫作hash_xxx。
1.vector 底层数据结构为动态数组 ,支持快速随机访问
2.list 底层数据结构为双向链表,支持快速增删
3.deque 底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问deque是一个双端队列(double-ended queue),也是在堆中保存内容的.它的保存形式如下:[堆1] --> [堆2] -->[堆3] --> …每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品.
4.stack 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
5.queue 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
6.priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
7.set 底层数据结构为红黑树,有序,不重复
8.multiset 底层数据结构为红黑树,有序,可重复
9.map 底层数据结构为红黑树,有序,不重复
10.multimap 底层数据结构为红黑树,有序,可重复
11.hash_set 底层数据结构为hash表,无序,不重复
12.hash_multiset 底层数据结构为hash表,无序,可重复
13.hash_map 底层数据结构为hash表,无序,不重复
14.hash_multimap 底层数据结构为hash表,无序,可重复

//注意这里是greater
max = multiset<int, greater<int> >;
max.insert(1);
max.insert(2);
max.begin(); //最大值
max.erase(max.begin()) //删除最大值

//对比之前的push_heap
vector<int> max;
max.push_back(num); //插入元素
push_heap(max.begin(),max.end(),less<int>()); //插入元素后需要调整维护最大根

pop_heap(max.begin(),max.end(),less<int>()); //弹出元素时需要先将pop_heap调整顺序
max.pop_back(); //再将末位弹出

3、字符串

3.1 数组与字符串的关系 20、46
leetcode 459

部分求和

42、57

剑指offer-42:连续子数组的最大和
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
**分析:**题干中已经给了提示了,就是如果向量中包含负数,是否应该包含某个负数?由于是连续数组的求和,必然是考虑滑窗算法,滑窗中纳入一个正数时,自然是会比原来的和更大,但是如果纳入了一个负数,就会缩小原来的和,是要从这里截止,还是继续纳入下一个数?假设在遍历到某个数x时,发现前面sum<0,那么纳入x时就有x>x+sum,之前的窗口不可能是最大和,那么滑窗还不如从当前这个数开始。进一步分析可以发现,如果某一个负数纳入进来时使得sum<0,那么窗口就得重新设置起点了,否者还是有必要纳入这个负数,可能后面的正数可能还会扩大和(只是可能)
需要注意的是,这里的重设起点其实只是在排除掉不可能是拥有最大值的窗口,要获知真正的最大值是多少,还是需要去记录一个最大值,在每一步的滑动过程中,需要一直将当前的和与保存的最大和进行比较,因为没有办法保证当前的和是不是最大了,以后还会不会遇到更大的。直到滑到最后一个数,才能下结论最大值是多少。

剑指offer-57

子序列问题

剑指offer42(在部分求和分析过),leetcode

子序列问题或者是子数组问题如果是要求是连续的同常就是要用滑窗方法,用滑窗需要解决的最大问题就是什么时候要重设起点而不继续向右延伸,明确了这个基本就可以解决问题了。如果是求极值的,通常需要保存一个当前最大值,并在每一次滑动中维护这个最大值。

如果不要求是连续的,一种是仍然用滑窗,那么这个原来的数组是可以经过修改的,怎么简单怎么修改,比如说做个排序如果能简化问题就排序。

leetcode-674:最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
分析:很简单的一道题,如果发现一个比它前面的数小,就重设起点。

leetcode-459:重复的字符串
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
分析:思路也很简单,就是中间一段代码,如果用这个重复子串作为滑窗,判断字符串是否可以由他构成。用
s[i*t]和s[i]进行比较。t表示的是子串的长度。注意t从1开始的,是把t前面的字符作为子串,不包括自己本身。

class Solution {
public:
     bool repeatedSubstringPattern(string s) {
        int len=s.size(),i;
        for(int t=1;t<=len/2;++t)
        {
            if (len%t) continue;    // 有余数,一定不为周期串
            for (i=t;i<len;++i){
                if(s[i%t]!=s[i])
                    break;
            }
            if (i==len) return true;
        }
        return false;
     }
};

leetcode-584:最长和谐子序列
和谐数组是指一个数组里元素的最大值和最小值之间的差别正好是1。现在,给定一个整数数组,你需要在所有可能的子序列中找到最长的和谐子序列的长度。
分析::1、不要求连续,尝试排序后滑窗。排序后去维护一个最右端比最左端最多大1的窗口,但是要维护这个窗口其实有很多的坑。首先,要求最大数比最小数大1,则一个合法的窗口应该至少有两个数,其次不能全都是一个数,那么如果在向右拓展时,一直碰到是同一个数,要不要将其纳入?我的做法是将其纳入,但是不更新最大值,直到找到一个比最左边数大1的数,将其纳入并且更新最大值。第三,如果向右遍历时当前数比左边界的数相差超过2,则左边界要左移,但是如果左移后,左边界还是太小,就一直要左移,有可能左移到左右边界相接,那么此时相当于从以这个相接点作为新的窗口起点,右边界要加一。
2、这里由于不要求是连续序列,可以彻底打乱甚至忽略数字的顺序,转为一个统计的题目。用一个有序映射存储每个数字有多少个,将映射表中相邻两个差值为1的key对应的value相加,找出这些和的最大值,即为所求。注意最好用map而不能用unordered_map,因为第二步是要轻易地获取相邻两个差值为1的key,用map在遍历时相邻key是有序的,用unordered_map是无序的,当然也不是不可以用,只不过会多了一步查询的操作,是用空间换时间。

leetcode-581:最短无序连续子数组
给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。你找到的子数组应是最短的,请输出它的长度。
**分析:**1、排序可以简化问题,但是需要额外的O(N)的存储空间。用两个指针一个从左一个从右向中间滑动,如果发现排序后与为排序的在当前位上相等,则左指针(右指针)进行右移(左移),直到两边同时不满足条件或者是,左右指针相接。返回的是right-left的值,但是如果左右相接了,返回0。逻辑很简单,但是死效率不高
2、(评论中看到一种神奇的方法,用自己的话复述一下)不需要排序,从头遍历数组,找到第一个升序数组的停止位置left,从尾部遍历数组,找到第一个降序数组的停止位置right,在这两个位置间找到最大值max和最小值min。从left的左边的升序数组找到min应当处于的位置,从right的右边的降序(从尾部数过来是降序,从头部数过来其实还是升序)找到max应当处于的位置,这两个位置之间就是最短无序数组。

3、特殊处理

3.2 部分求和 42、57

3.3 统计 56

3.4 特殊概念 49、51

你可能感兴趣的:(学习笔记)