1、数组
随机存取,连续,相同数据类型
代码随想录数组理论基础
2、二分法
2.1 leetcode 704:二分法寻找非重复元素
第一遍代码,递归
class Solution {
public:
int start = 0;
int search(vector<int>& nums, int target) {
int ii = nums.size()/2;
if(nums.size() == 0) {
return -1;
}
while(nums[ii] > target) {//大于还是小于想清楚
vector<int> a;
for(int j = 0; j < ii; j++) {
a.push_back(nums[j]);
}
return search(a, target);
}
while(nums[ii] < target) {
start += ii + 1;
vector<int> a;
for(int j = ii+1; j < nums.size(); j++) {
a.push_back(nums[j]);//a中没有值不能直接a[ii-j-1]
}
return search(a, target);
}
if(nums[ii] == target) {
return start + ii;
}
return -1;
}
};
通过下标对数组操作即可不需要反复递归
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
int middle = (left+right)/2;//使用下标避免递归传入新数组,通过下标对数组操作;如果left和right数值都很大的话,left+right相加会溢出,建议使用left + ((right - left) / 2
while(left<right)
{
middle = (left+right)/2;
if(nums[middle] == target)
{
return middle;
}
else if(nums[middle]>target)
{
right = middle;
}
else
{
left = middle + 1; // 注意这里的if条件 与 left / right变动情况,别写反了
}
}
return -1;
}
};
2.2 使用二分法特征/前提
1、强调数组中无重复元素(重复也可考虑,转化为求第一个/最后一个的边界,因为除了刚开始初始值之外,往后在left左边的一定小于left,在right右边的一定大于right;例如34),因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的
2、另一个前提是数组是有序数组,这也是使用二分查找的基础条件
这些都是使用二分法的前提条件,当看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了
2.3 使用二分法注意点
while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则,区间的定义一般为两种,左闭右闭即[left, right](就是下面代码),或者左闭右开即[left, right)(就是上面代码),复杂度为 O(log n)
// 左闭右闭
class Solution {
public:
int search(vector<int>& nums, int target) {
int minNum = 0;
int maxNum = nums.size() - 1;
while (minNum <= maxNum) {
int middle = (minNum + maxNum) / 2;
if (nums[middle] == target) {
return middle;
}
else if (nums[middle] < target) {
minNum = middle + 1;
}
else {
maxNum = middle - 1;
}
}
return -1;
}
};
2.4 leetcode 35:二分法寻找插入位置
二分法:
分别处理如下四种情况
(注意)目标值在数组所有元素之前 [0, -1] return right + 1 / left
目标值等于数组中某一个元素 return middle;
目标值插入数组中的位置 [left, right],return right + 1 / left
目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1 / left
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle;
}
}
return left; // 应插入位置就是left的位置
}
};
也可以使用暴力解决
暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。
for (int i = 0; i < nums.size(); i++) {
// 分别处理如下三种情况
// 目标值在数组所有元素之前
// 目标值等于数组中某一个元素
// 目标值插入数组中的位置
if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
return i;
}
}
2.5 leetcode 34:二分法寻找重复元素的边界
第一次代码,多种提交出现情况后多次加补丁,没有以正确方式有条理地考虑问题
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> result;
int start = 0;
int end = nums.size() - 1;//前闭后闭
int middle = start + (end - start)/2;
while(start <= end) {
middle = start + (end - start)/2;
if(nums[middle] > target) {
end = middle - 1;
}
else if(nums[middle] < target) {
start = middle + 1;
}
else {
int i;
int judge = 0;//如果有三个连续的如[3,3,3],target=3只记录一头一尾
for(i = start; i <= end; i++) {
if(nums[i] == target) {
if(judge == 0) {
result.push_back(i);
judge = 1;
}
}
else if(result.size() != 0){//判断nums[i] != target时要已经之前有过等的才跳出
break;//如果不等了要及时跳出,因为下面判断一个时要用到i
}
}
if(result.size() == 1) {
result.push_back(i-1);//补充特殊情况只有一个时,如nums=[1],target=1; nums=[1,3], target=1,注意i加了1了,要减掉
}
return result;
}
}
result.push_back(-1);
result.push_back(-1);
return result;
}
正确的思路:
二分法 重复也可考虑,转化为求第一个/最后一个的边界,因为除了刚开始初始值之外(情况一要单独看),往后在left左边的一定小于left,在right右边的一定大于right
以左右边界为考虑对象,需要将情况考虑仔细了:
把所有情况都讨论一下。
寻找target在数组里的左右边界,有如下三种情况:
情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
二分查找,寻找target的右边界(不包括target)
接下来,在去寻找左边界,和右边界了。
采用二分法来去分别寻找左右边界,关键是当nums[middle] == target的时候 的处理,见注释
代码随想录实现方式:
寻找右边界:
// 如果rightBorder为没有被赋值(即target在数组范围的左边,例如数组[3,3],target为2),为了处理情况一
int getRightBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) { // 当left==right,区间[left, right]依然有效
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else { // 当nums[middle] == target的时候,更新left,这样才能得到target的右边界(如果有target的话最后一定过了)
left = middle + 1;//只要小于等于target就往右移,直到left超过right,这样一定在右端,二分法只有不断对 等于的 往右推才能覆盖有重复的情况
rightBorder = left;
}
}
return rightBorder;
}
左边界同理:
int getLeftBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
} else {
left = middle + 1;
}
}
return leftBorder;
}
对三种情况的处理:
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 情况一
if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
// 情况二
return {-1, -1};
自己实现方式:
class Solution {
public:
int getRightboard(vector<int>& nums, int target) { // 寻找右边界,直接返回的就是右边界的值
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
if (nums[middle] > target) {
right = middle - 1;
}
else if (nums[middle] < target) {
left = middle + 1;
}
else {
left = middle + 1; // 与普通二分的区别,相等的时候left也要往右推,等right减过left,left再推过right 或 left直接推过right
}
}
return right;// 情况一不需要特殊处理,因为right自然会不停减1,直到-1
}
int getLeftboard(vector<int>& nums, int target) { // 寻找左边界,直接返回的就是左边界的值
int left = 0;
int right = nums.size() - 1;
if (nums.size() > 0 && nums[0] > target) return -1; // 情况一需要特殊处理,因为left最小就是0
while (left <= right) {
int middle = left + (right - left) / 2;
if (nums[middle] > target) {
right = middle - 1;
}
else if (nums[middle] < target) {
left = middle + 1;
}
else {
right = middle - 1;
}
}
return left;
}
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> res;
int right = getRightboard(nums, target);
int left = getLeftboard(nums, target);
if(left > right) { // 情况二,整体代码退化到35
res.push_back(-1);
res.push_back(-1);
}
else { // 情况一,三
res.push_back(left);
res.push_back(right);
}
return res;
}
};
2.6 leetcode 69:二分法寻找平方根
二分法寻找的部分中:
int start = 0;
int end = x;
int middle = start + (end - start)/2;
while(start < end) {
middle = start + (end - start)/2;
if((long long)middle * middle > x) {//if的前后与结果是偏大偏小无关,middle的取值才有关,middle位置的值决定了start/end谁动
end = middle - 1;
}
else if((long)middle * middle < x) {
start = start + 1;
}
else {
return middle;
}
}
return end;
}
};
出现Memory Limit Exceeded情况,可以 直接用值 不需要把值放在数组里
没必要做成一个数组,直接用middle平方比,注意middle平方可以用long long数据类型防止越界。
注意不可以写成(long long)(middle*middle),因为这样会先算middle*middle,int显然可能超范围
对于无法控制二分法查找偏大偏小问题,只记录left侧的答案即可,无论小于等于都动left,因为left对应记录一次答案所以等于条件也要放入其中
class Solution {
public:
int mySqrt(int x) {
int start = 0;
int end = x;
int middle;
int ans;//记录结果
while(start <= end) {//一定加等号因为可能end等于x
middle = start + (end - start)/2;
if((long long)middle*middle <= x) {//注意不可以写成(long long)(middle*middle),因为这样会先算middle*middle,int显然可能超范围
ans = middle;
start = middle + 1;
}
else {
end = middle - 1;
}
}
return ans;
}
};
不能用上一题的思路,因为 上一题 就算找到了目标 左边界还是往前退一格的
也不能 写成右边界的情况,因为目标是小一(8右边界的结果是3)
middle = start + (end - start)/2;
if((long long)middle*middle < x) {
start = middle + 1;
}
else {
ans = middle;
end = middle - 1;
}
3、双指针
3.1 leetcode 27:双指针的两种思路
写法一:使用同向一快一慢两个指针:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int j = 0;
for(int i = 0; i < nums.size(); i++) {//双指针
if(nums[i] != val) {
nums[j++] = nums[i];
}
}
nums.resize(j);
return j;
}
};
写法二:左右相向而行的两个指针,不容易想到,元素顺序改变了
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
// 找左边 等于 val的元素,一定要确保leftindex<=rightindex
while (leftIndex <= rightIndex && nums[leftIndex] != val){
++leftIndex;
}
// 找右边 不 等于val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
-- rightIndex;
}
// 将右边 不等于val的元素覆盖左边 等于val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
return leftIndex; // leftIndex一定指向了最终数组末尾的下一个元素
}
};
自己实现:
left < right的话最后要返回left+1,因为left直接停在最后一个元素的下标处,但由此带来一个问题,对于输入为空的时候他也+1了,为了统一逻辑,需要left <= right,返回left
这边要判断left是否小于right,因为等于的那个其实不需要记录,记录了left就大1了且多赋了一次值,考虑[3,2,2,3],输出[2,2,2],预期结果[2,2](如果输入不为空的话,其实应该left < right)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) { // left < right的话最后要返回left+1,因为left直接停在最后一个元素的下标处,但由此带来一个问题,对于输入为空的时候他也+1了,为了统一逻辑,需要left <= right,返回left
while (left <= right && nums[left] != val) {
left++;
}
while (left <= right && nums[right] == val) {
right--;
}
if (left < right)
nums[left++] = nums[right--]; // 这边要判断left是否小于right,因为等于的那个其实不需要记录,记录了left就大1了且多赋了一次值,考虑[3,2,2,3],输出[2,2,2],预期结果[2,2]
}
return left;
}
};
3.2 leetcode 844:两个字符串分别使用同向快慢指针
两个字符串分别使用同向快慢指针,其中慢指针还会回退
第一遍代码:
class Solution {
public:
bool backspaceCompare(string s, string t) {
int j1 = 0;
for(int i1 = 0; i1 < s.size(); i1++) {//一定要从0开始,从1开始的话c#d#会有问题
if(s[i1] == '#' && j1 == 0) {//对特殊情况进行处理,对空#仍为空
continue;
}
if(s[i1] == '#') {
j1--;
}
else {
s[j1++] = s[i1];
}
}
s.resize(j1);
int j2 = 0;
for(int i2 = 0; i2 < t.size(); i2++) {
if(t[i2] == '#' && j2 == 0) {
continue;
}
if(t[i2] == '#') {
j2--;//目标位置进行回退(最后resize一下相当于删除了)
}
else {
t[j2++] = t[i2];
}
}
t.resize(j2);
return s==t;//compare:若参与比较的两个串值相同,则函数返回 0;若字符串 S 按字典顺序要先于 S2,则返回负值;反之,则返回正值。下面举例说明如何使用 string 类的 compare() 函数。
}
};
为什么不能用compare(compare用法):
若参与比较的两个串值相同,则函数返回 0;若字符串 S 按字典顺序要先于 S2,则返回负值;反之,则返回正值。下面举例说明如何使用 string 类的 compare() 函数。
第二次错误的思路:
class Solution {
public:
bool backspaceCompare(string s, string t) {
int s1 = s.size() - 1; // 不为空才可以这么写
int t1 = t.size() - 1; // 从后往前考虑有个问题,如果出现"ab##"就会失效,所以只能老老实实遇到一个退格删一个,对单一字符串分别使用同向双指针
while (s1 >= 0 && t1 >= 0) {
if (s[s1] == '#')
s1 -= 2;
if (t[t1] == '#')
t1 -= 2;
if(s1 >= 0 && t1 >= 0 && s[s1] != t[t1])
return false;
s1--;
t1--;
}
if (s1 == t1) // 考虑第一个字母都是退格
return true;
return false;
}
};
第二次错误的实现:
class Solution {
public:
bool backspaceCompare(string s, string t) {
int low = 0;
for (int fast = 0; fast < s.size(); fast++) {
if(low == 0 && s[fast] == '#') // 注意low = 0,不是fast = 0,s[fast] == '#',不是s[0] == '#' 考虑输入"a##c"理解
continue;
if(s[fast] != '#') {
s[low++] = s[fast];
}
else {
low--;
}
}
s.resize(low);
low = 0;
for (int fast = 0; fast < t.size(); fast++) {
if(low == 0 && t[fast] == '#')
continue;
if(t[fast] != '#') {
t[low++] = t[fast];
}
else {
low--;
}
}
t.resize(low);
return s==t;
}
};
3.3 leetcode 977:相向而行的双指针
采用相向而行的双指针(因为在待放入结果的数列里面只有左右两边的数可能是最大的,所以从大到小排序)
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result(nums.size());
int z = nums.size() - 1;
int j;
for(int i = 0, j = nums.size() - 1; i <= j; ){
if(nums[i] * nums[i] >= nums[j] * nums[j]){
result[z--] = nums[i] * nums[i];
i++;
}
else{
result[z--] = nums[j] * nums[j];
j--;
}
}
return result;
}
};