数组类型的题目是一种非常普遍的题目,根据题目不一样的,解决的策略也有很多,今天主要讲的是采用左右指针和扫描线来解决的题目类型。
左右指针
左右指针一般用来解决区间内某个值得问题,看上去非常相类似DP问题,但是它和DP又有本质的区别,归纳为有以下两个特点:
- 一个区间内或者两个元素的最大,最小,和,或者中间值,求区间内的某个数:比如求两个水柱之间最大水量,求window下的中间数,求一个数组指定和的组合
- 左右指针是不断逼近最终结果策略,而DP是从某个或者某几个中间状态可以求到最终状态。
整个左右指针又可以分为三个类型:
- 左右夹击类型
- 追赶类型
- 划分类型
夹击型
求数组中所有和为指定数的两个数?two sum
Example
numbers=[0,2,11,15,7], target=9
return [2, 7]
如果可以用附加数据结构,采用hashmap是最快的办法,可以O(n)内解决这个问题。具体做法是依次遍历数组中元素,当遍历到A[i]时,判断下target-A[i]是否在hashmap中存在,存在则直接将这两个元素插入到结果中,如果不存在则将A[i]元素压入到hashmap中,直到遍历完所有元素。
如果不能用附加数据结构,则就需要用左右指针方法,先对整个数组进行排序,快排就是一种in-place的排序算法,然后将left指针放在开始的位置,right指针放在末尾的位置
A[left]+A[right]==target则找到这样的组合
A[left]+A[right]>target则right–,因为只有right–才能将和降下来
A[left]+A[right]
class Solution {
public:
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1+1, index2+1] (index1 < index2)
*/
vector<int> twoSum(vector<int> &nums, int target) {
vector<int> Result;
if (nums.size() < 2) {
return Result;
}
vector<int> NumPos(nums.size());
for (int i = 0; i < NumPos.size(); ++i) {
NumPos[i] = i;
}
sort(NumPos.begin(), NumPos.end(), [&nums](int Left, int Right) { return nums[Left] < nums[Right]; });
int Left = 0;
int Right = NumPos.size() - 1;
while (Left < Right) {
if (nums[NumPos[Left]] + nums[NumPos[Right]] == target) {
if (NumPos[Left] > NumPos[Right]) {
Result.push_back(NumPos[Right]);
Result.push_back(NumPos[Left]);
}
else {
Result.push_back(NumPos[Left]);
Result.push_back(NumPos[Right]);
}
break;
}
else if (nums[NumPos[Left]] + nums[NumPos[Right]] > target) {
Right--;
}
else {
Left++;
}
}
return Result;
}
};
这道题使我想到杨氏矩阵,寻找某个target的值,将开始指针放在矩阵的右上角,如果target数比当前数小,则指针往左走,如果target数比当前述大,则指针往下走,直到指针走出整个矩阵。
注水问题
给出水柱高度,求两个水柱之间的最大注水面积
两个水柱之间的注水面积,求这两根水柱。
left指向0,right指向最后一个位置,最大面积为min(A[left], A[right])*(right-left)
A[left]>=A[right] right–,因为要有更大的面积只能提高柱子的高度,提高
A[left]
class Solution {
public:
/**
* @param heights: a vector of integers
* @return: an integer
*/
int maxArea(vector<int> &heights) {
if( heights.size() < 2 ){
return 0;
}
int left = 0;
int right = heights.size() - 1;
int maxContainer = 0;
while(left < right){
maxContainer = max(maxContainer, min(heights[left], heights[right])*(right-left));
if( heights[left] > heights[right] ){
right--;
}else{
left++;
}
}
return maxContainer;
}
};
注水问题的另外一个经典问题,给出水柱高度,求能够注入的水面积
Example
Given [0,1,0,2,1,0,1,3,2,1,2,1], return 6.
left指针指向0,right指针指向末尾,注水线为secHeight=min(A[left],A[right])
A[left]>=A[right] right的注水线为secHeight=max(secHeight,A[right])
注水面积为secHeight-A[right]
right–
A[left]
class Solution {
public:
/**
* @param heights: a vector of integers
* @return: a integer
*/
int trapRainWater(vector<int> &heights) {
if (heights.size() < 3) {
return 0;
}
int sum = 0;
int left = 0;
int right = heights.size() - 1;
int secHeight = min(heights[left], heights[right]);
while(left < right)
{
if(heights[left] < heights[right]){
secHeight = max(heights[left], secHeight);
sum += secHeight - heights[left];
left++;
}else{
secHeight = max(heights[right], secHeight);
sum += secHeight - heights[right];
right--;
}
}
return sum;
}
};
整个注水面积等于每个柱子的注水面积之和,每个柱子的注水面积等于注水高度减去柱子高度,那就归结为求注水高度,注水高度为当前柱子到左边的最大值leftHeight,当前柱子到右边的最大值rightHeight,然后min(leftHeight, rightHeight)为注水高度。
class Solution {
public:
/**
* @param heights: a vector of integers
* @return: a integer
*/
int trapRainWater(vector<int> &heights) {
if(heights.size() < 3){
return 0;
}
vector<int> leftBar(heights.size());
leftBar[0] = heights[0];
for(int i = 1; i < heights.size(); i++){
leftBar[i] = max(heights[i], leftBar[i-1]);
}
vector<int> rightBar(heights.size());
rightBar[heights.size()-1] = heights[heights.size()-1];
for(int i = heights.size()-2; i >= 0; i--){
rightBar[i] = max(heights[i], rightBar[i+1]);
}
int res = 0;
for(int i = 0; i < heights.size(); i++){
res += min(leftBar[i], rightBar[i]) - heights[i];
}
return res;
}
};
通过左右扫描就可以求出最终面积
小结
夹击类型一般需要两个界才能求出最终结果,可以通过判断左右指针大小来判断走向
追击问题
左右指针开始在同一个位置,然后left追赶right
求两个数组的最小差值
Example
For example, given array A = [3,6,7,4], B = [2,8,9,3], return 0
做法是先对数组A和数组B进行排序,然后left指向A的开始位置,right指向B开始位置
A[left]>B[right] difference=A[left]-B[right]逼近需要right++,才能将difference降低
A[left]
class Solution {
public:
/**
* @param A, B: Two integer arrays.
* @return: Their smallest difference.
*/
int smallestDifference(vector<int> &A, vector<int> &B) {
if( A.empty() || B.empty() ){
return 0;
}
sort(A.begin(), A.end());
sort(B.begin(), B.end());
auto difference = numeric_limits<int>::max();
int i = 0;
int j = 0;
while( i < A.size() && j < B.size() ){
if( A[i] > B[j] ){
difference = min(difference, A[i]-B[j]);
j++;
}else if( A[i] < B[j] ){
difference = min(difference, B[j] - A[i]);
i++;
}else{
return 0;
}
}
return difference;
}
};
求最长无重复子串
Example
For example, the longest substring without repeating letters for “abcabcbb” is “abc”, which the length is 3.
For “bbbbb” the longest substring is “b”, with the length of 1.
left和right指向开始位置,然后移动right,直到left和right之间有重复字符,记录一个最大无重复子串,此时移动left直到没有重复字符,可以不移动left,直接将重复字符位置的下一个位置赋值给left,此时left和right就没有重复字符了。再次移动right,重复此操作。
class Solution {
public:
/**
* @param s: a string
* @return: an integer
*/
int lengthOfLongestSubstring(string s) {
if( s.empty() ) {
return 0;
}
unordered_map<char, int> charPos;
int res = 0;
int left = 0;
int right = 0;
while( right < s.size() ) {
if( (charPos.end() != charPos.find(s[right])) && (charPos[s[right]] >= left) ) {
left = charPos[s[right]] + 1;
}
charPos[s[right]] = right;
right++;
res = max( res, right-left );
}
return res;
}
};
小结
需要通过两个界来确定最终结果,根据某种内部判断来确定是left增长还是right增长。
求数组内符合某种条件的和,一般是通过左右扫描。对于求数组范围内符合某种条件的最大最小值,就需要看趋势,是左右夹击还是追赶。
划分类型
划分类型是通过某种策略对整个数组进行排序
Move Zeros
将一个数组中非0元素放在左边,同时保持它们的相对位置,0放在数组右边。
如果是单纯的划分,可以采用quicksort中的partition策略,但是因为要保持非0元素放在左边,0放在右边,但是它不能保持非0元素的顺序,这也是为什么quicksort是一种不稳定的算法。
换种思路来考虑这种问题,将所有的非0元素按照相对顺序放在数组的左边,剩余的元素直接赋值为0就可以了。
具体实现就是writePos表示待插入位置,readPos表示待遍历的位置,依次遍历完所有元素,如果readPos是0则直接跳过,如果非0,将其赋值到writePos同时writePos++,最后writePos到数组末尾的所有元素直接赋值为0即可
class Solution {
public:
void moveZeroes(vector<int>& nums) {
if(nums.empty()){
return;
}
int writePos = 0;
for(int i = 0; i < nums.size(); i++){
if(nums[i] != 0){
nums[writePos++] = nums[i];
}
}
for(;writePos < nums.size(); writePos++){
nums[writePos] = 0;
}
}
};
还有一种优化策略,就是要保持原有非零元素的相对顺序,从位置上看writePos是所有非0元素尾部的下一个位置,也是第一个0的位置,交换readPos和writePos的元素后这样能满足writePos前所有非0元素的相对位置,也能将0交换到右边。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
if(nums.empty()){
return;
}
int writePos = 0;
for(int i = 0; i < nums.size(); i++){
if(nums[i] != 0){
swap(nums[writePos++], nums[i]);
}
}
}
};
sort color
有0,1,2三种颜色的数组,将其排好序。
Example
Given [1, 0, 1, 2], sort it in-place to [0, 1, 1, 2].
采用辅助数组的办法就是采用桶排序,计算出0的个数,1的个数和2的个数,最后按照个数直接赋值到整个数组。
另外一个策略就是采用左右指针,左指针指向的是0要插入的位置,right表示2要插入的位置,中间的位置则为1了。
依次遍历整个数组
A[i]=0则将A[i]和A[left]对换,left++
A[i]=2则将A[i]和A[right]对换,right–
A[i]=1则i继续往后移动
直到i和right相遇
class Solution{
public:
/**
* @param nums: A list of integer which is 0, 1 or 2
* @return: nothing
*/
void sortColors(vector<int> &nums) {
if( nums.empty() ) {
return;
}
int Left = 0;
int Right = nums.size() - 1;
int i = 0;
while( i <= Right ) {
if( 0 == nums[i] ) {
swap(nums[Left], nums[i]);
Left++;
i++;
}else if( 1 == nums[i] ){
i++;
}else {
swap(nums[i], nums[Right]);
Right--;
}
}
}
};
这道题使我想起快排中的partition。
如果这个color有k个,求排序
方法也是一样的,将compareIndex设置为2,左右指针,left是小于compareIndex,right是大于compareIndex,中间就是等于compareIndex,这样每次就可以划分出1和2,剩下就是再次划分right部分,每次compareIndex+=2
class Solution {
public:
/**
* @param colors: A list of integer
* @param k: An integer
* @return: nothing
*/
void sortColors2(vector<int> &colors, int k) {
if (colors.empty() || k < 2) {
return;
}
int Begin = 0;
int End = colors.size() - 1;
int ComparedIndex = 2;
while ((Begin < colors.size()) && (ComparedIndex <= k)) {
int i = Begin;
End = colors.size() - 1;
while (i <= End) {
if ((i >= Begin) && (colors[i] < ComparedIndex)) {
swap(colors[i], colors[Begin]);
Begin++;
}
else if ((i <= End) && (colors[i] > ComparedIndex)) {
swap(colors[i], colors[End]);
End--;
}
else {
i++;
}
}
Begin = End + 1;
ComparedIndex += 2;
}
}
};
nuts and bolts
给出两个数组一个是nuts,一个是bolts,nuts和bolts组内不能比较,只能nuts和bolts之间进行比较,比较的结果为如果匹配返回0,如果nut大于bolt则返回1,如果nut小于bolt则返回-1,如果不是nut和bolt则返回2。
类似sort color思想,先根据nuts第一个元素,将bolts划分为小于等于和大于nuts[i]三个部分,会得到一个bolts的划分点boltPos,再根据bolts划分点再对nuts进行同样的排序会得到一个nutPos,这样bolts[boltPos]和nuts[nutPos]相等,小于的都会在boltPos和nutPos的左边,大于的都会在boltPos和nutPos的右边,然后再对整个nuts和bolts进行排序。
class Solution {
public:
/**
* @param nuts: a vector of integers
* @param bolts: a vector of integers
* @param compare: a instance of Comparator
* @return: nothing
*/
void sortNutsAndBolts( vector< string> & nuts, vector< string> & bolts, Comparator compare ) {
if ( nuts.empty() || nuts.size() != bolts.size()) {
return;
}
QSort( nuts, bolts, compare, 0, nuts.size()-1);
}
void QSort( vector< string> & nuts, vector< string> & bolts, Comparator compare , int begin , int end ) {
if ( begin >= end) {
return;
}
auto partPos = Partition( nuts, bolts[begin ], begin, end, compare);
partPos = Partition( bolts, nuts[partPos], begin, end, compare);
QSort( nuts, bolts, compare, begin, partPos - 1);
QSort( nuts, bolts, compare, partPos + 1, end);
}
int Partition( vector< string>& partArray, string pivot, int begin, int end, Comparator compare ) {
if ( begin >= end) {
return begin;
}
for ( int i = begin; i <= end; i++) {
if ( compare.cmp( partArray[i], pivot) == 0 || compare.cmp(pivot , partArray [i ]) == 0) {
swap( partArray[i], partArray[ begin]);
break;
}
}
auto now = partArray[begin ];
int left = begin;
int right = end;
while (left < right)
{
while (left < right && ( compare.cmp( partArray[right], pivot) != -1 || compare.cmp(pivot , partArray [right ]) != 1))
{
right--;
}
partArray[left] = partArray[ right] ;
while (left < right && ( compare.cmp( partArray[left], pivot) != 1 || compare.cmp(pivot , partArray [left ]) != -1))
{
left++;
}
partArray[right] = partArray[ left] ;
}
partArray[left] = now;
return left;
}
}
小结
左右两个指针是待插入的位置,根据某种条件判定遍历的元素是写入left还是写入right。
总结
左右指针问题总的来说有两种类型,一种类型是定界问题,根据左右两个边界确定最终结果,夹击类型就是通过判定左右两边的值来判定left和right的走向,另外一种类型就是排序,划分型排序,就是快排中的排序,一般针对有限种元素排序或者给出比较函数来判定大小进行排序。所以从题型上看有点类似DP,但是本质上是有很大的区别,它不需要中间状态。