文章目录
1365. 有多少小于当前数字的数字
暴力解法
两层for循环,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
class Solution {
public:
vector<int> smallerNumbersThanCurrent(vector<int>& nums) {
//暴力解法 两层for循环
vector<int> result;
for(int i=0; i<nums.size(); i++)
{
int count = 0;
for(int j=0; j<nums.size(); j++)
{
if(nums[j] < nums[i]) count++;
}
result.push_back(count);
}
return result;
}
};
排序
排序加哈希,时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)
关键点
- 定义一个新数组,从小到大排序。排序后,每一个数值的下标代表这前面有几个比它小的数了。
- 用一个哈希表hash做数值和下标的映射,通过数值快速查询下标,即统计有多少个比当前下标对应数值小的数。
- 针对数值相同的情况,构造hash数组时,从后向前遍历,保证hash里存放的是相同元素最左面的数值和下标。
C++实现
class Solution {
public:
vector<int> smallerNumbersThanCurrent(vector<int>& nums) {
vector<int> result = nums;
sort(result.begin(), result.end());
//哈希数组 下标与数值映射 倒序遍历
int hash[101];
for(int i=result.size()-1; i>=0; i--) hash[result[i]] = i;
//遍历原数组 统计结果
for(int i=0; i<nums.size(); i++)
{
//通过原数组数值nums[i]找到在哈希中对应下标i
//该下标i代表前面有i个比nums[i]大的数
result[i] = hash[nums[i]];
}
return result;
}
};
计数统计
频次数组result,统计数字i出现的次数,比数字i小的数目就是result[0]到result[i-1]的和
class Solution {
public:
vector<int> smallerNumbersThanCurrent(vector<int>& nums) {
//频次数组
vector<int> result;
vector<int> count(101, 0);
//统计出现的频次
for(int i=0; i<nums.size(); i++) count[nums[i]]++;
//累计求和 比数字i小的就是count[0]-count[i-1]求和
for(int i=1; i<=100; i++) count[i] += count[i-1];
//保存结果
for(int i=0; i<nums.size(); i++)
{
if(nums[i]==0) result.push_back(0);//没有比数字i大的数
else result.push_back(count[nums[i] - 1]);
}
return result;
}
};
941. 有效的山脉数组
- 判断是山峰,就是要保证左边到中间,和右边到中间是递增的。同理,判断山谷,就是要保证左边到中间,和右边到中间是递减。
- 左右双指针,如果中间相遇,说明有山谷
- 左右指针不要越界,并且左指针或者右指针没有移动说明是一个单调递增或者递减的数组,不是山峰
class Solution {
public:
bool validMountainArray(vector<int>& arr) {
if(arr.size() < 3) return false;
int left = 0;
int right = arr.size() - 1;
//注意左右指针不要越界
while(left < arr.size()-1 && arr[left] < arr[left+1]) left++;
while(right > 0 && arr[right] < arr[right-1]) right--;
if(left == right && left!=0 && right!=arr.size()-1) return true;
return false;
}
};
1207.独一无二的出现次数
统计频次,判断频次中是否有重复的数字;注意负数的处理
class Solution {
public:
bool uniqueOccurrences(vector<int>& arr) {
//数组长度是2000,有负数+1000
int count[2002] = {0};
for(int i=0; i<arr.size(); i++) count[arr[i] + 1000]++;
//频次数组判断重复元素
bool hash[1002] = {false};//默认重复出现,记录出现过的元素
for(int i=0; i<=2000; i++)
{
if(count[i])//arr[i]出现过
{
//独一无二 哈希数组记录
if(hash[count[i]] == false) hash[count[i]] = true;
else return false;//说明之前记录过 重复出现
}
}
return true;
}
};
283. 移动零
前后双指针 两次循环
- 前后双指针同时移动,如果遇到0元素,后指针先前移,前指针不动,并把后指针指向元素覆盖前指针指向元素。
- 0元素的个数就是nums.size() - 前指针
- 时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int preindex = 0;
for(int curindex = 0; curindex<nums.size(); curindex++)
{
//只要是非0元素 curindex元素往前覆盖preindex
//如果是0 curindex前移 preindex不移动
if(nums[curindex] != 0)
{
nums[preindex++] = nums[curindex];
}
}
//preindex~nums.size()-1 是0的个数
for(int i=preindex; i<nums.size(); i++)
{
nums[i] = 0;
}
}
};
快速排序 一次循环
参考快速排序思想,双指针,以0为中间点,如果遇到0元素,交换元素位置。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
//快速排序 1次循环
if(nums.size() <= 1) return;
int slow = 0;
for(int fast = 0; fast<nums.size(); fast++)
{
if(nums[fast] != 0)
{
if(fast > slow)//避免了fast = slow
{
nums[slow] = nums[fast];
nums[fast] = 0;
}
slow++;
}
}
}
};
189. 旋转数组
局部反转+整体反转
时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
- 右旋转,优先反转整个字符串,步骤如下:
- 反转整个字符串
- 反转前k个字符
- 反转后k个字符
- 需要注意的是,如果k大于nums.size,右移 k % nums.size() 次。例如,1,2,3,4,5,6,7,如果右移动15次变成7 1 2 3 4 5 6 。相当于右移15 % 7 = 1次,即k % nums.size() 次。
- 同理,左旋转,反转前k个字符;反转后k个字符;反转整个字符串
整体右移 额外空间
需要一个新数组,整体右移,整体替换,时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
class Solution {
public:
void rotate(vector<int>& nums, int k) {
//方法二 额外空间 整体右移k
vector<int> temp(nums.size());
for(int i=0; i<nums.size(); ++i)
{
temp[(i+k) % nums.size()] = nums[i];
}
nums.assign(temp.begin(), temp.end());//拷贝
}
};
环状替换
- 整体替换需要额外的数组空间,主要是为了避免被替换位置的元素被覆盖。
- 例如,[1,2,3],整体右移时,如果不加额外的空间,1换到3元素位置时,元素3会被覆盖二丢失。那么为了节省空间开销,先临时保存元素3,再交换元素1和3。
- 核心思路:每处理一个位置i,先用cur和curindex记录当前元素及其位置,也就是上一个覆盖元素及其位置,再跳转到新位置nextindex,交换新位置元素与cur,还需要判断是否遍历过了整个环形。
- 为了访问到所有的元素,要遍历的次数是k和n的最小公约数gcd(k, n),单次遍历会访问到**n / gcd(k, n)**个元素i从头遍历.
- 一种写法是通过下标来模拟上述过程,如果i == curindex,说明遍历了整个环形的元素,结束遍历。另一种写法是用count变量记录遍历过元素数量,如果count=n,说明遍历了整个数组,结束遍历。
写法1——下标判断
class Solution {
public:
void rotate(vector<int>& nums, int k) {
//方法三
int n = nums.size();
k = k % n;
int numer = gcd(k, n);//最大公约数 需要遍历的总次数
for(int i=0; i<numer; i++)
{
int curindex = i;//记录当前元素位置
int cur = nums[i];//记录当前元素值
//先执行再判断
do
{
int nextindex = (curindex + k) % n;//需要转移的位置
swap(nums[nextindex], cur);//交换元素
curindex = nextindex;
}while(i != curindex);
}
}
};
写法2——统计遍历元素个数
class Solution {
public:
void rotate(vector<int>& nums, int k) {
//方法三 另一种写法
int n = nums.size();
k %= n;
int numer = gcd(k, n);//需要遍历的总次数
int count = n / numer;//单次遍历的元素个数
for (int i = 0; i <numer; i++) {
int curindex = i, cur = nums[curindex];
int flag = 0;
while (flag < count) {
curindex = (curindex + k) % n;
swap(nums[curindex], cur);
flag++;
}
}
}
};
724.寻找数组的中心下标
- 先求总和sum,再求左边和leftsum,如果右边和rightsum + leftsum + nums[i] = sum,说明找到中心位置i,也可以是左边和leftsum*2+中心索引值 nums[i] = sum
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int sum = 0;
for (int num : nums) sum += num;
int leftsum = 0;//leftsum=rightsum
for(int i=0; i<nums.size(); i++)
{
if(nums[i] == (sum - leftsum*2)) return i;
leftsum += nums[i];
}
return -1;
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
直接统计
直接记录与目标值相等的元素位置,再处理结果
- 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//方法1
vector<int> result;
for(int i = 0; i<nums.size(); i++)
{
if(nums[i]==target) result.push_back(i);
}
//多种情况处理
if(result.size() == 0) return {-1, -1};//不存在
else if(result.size() == 1) return {result[0], result[0]};//有一个
else if(result.size() == 2 && result[0] > result[1]) swap(result[0], result[1]);//有2个
else return {result[0], result[result.size()-1]};
return result;
}
};
二分查找 分开查找
- 找出左右边界,熟记二分模板写法,时间复杂度 O ( log n ) O(\log n) O(logn)
- 三种情况:
- 情况1,目标值在区间外
- 情况2 目标值在区间内但不存在
- 情况3 目标值在区间内且存在
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int leftboard = getLeftBoard(nums, target);
int rightboard = getRightBoard(nums, target);
//情况1 目标值在区间外
if(leftboard == -2 || rightboard == -2) return {-1, -1};
//情况3 目标值在区间内且存在
if(rightboard - leftboard > 1) return {leftboard + 1, rightboard - 1};
return {-1, -1};//情况2 目标值在区间内但不存在
}
private:
//二分查找模板1 寻找右边界
int getRightBoard(vector<int>& nums, int target)
{
int left = 0, right = nums.size()-1;
int rightboard = -2;
while(left <= right)
{
int mid = left + (right - left) / 2;
//目标值在左区间 左边界取最右值
if(nums[mid] > target) right = mid - 1;
else
{
left = mid + 1;//nums[middle] == target的时候更新left
rightboard = left;
}
}
return rightboard;
}
//二分查找模板2 寻找左边界
int getLeftBoard(vector<int>& nums, int target)
{
int left = 0, right = nums.size()-1;
int leftboard = -2;
while(left <= right)
{
int mid = left + (right - left) / 2;
//目标值在左区间 左边界取最右值
if(nums[mid] >= target)
{
right = mid - 1;//nums[middle] == target的时候更新right
leftboard = right;
}
else left = mid + 1;
}
return leftboard;
}
};
二分查找 合并查找 写法1
- 根据输入的字段查找左右边界,同时查找左右边界
- 如果找左边界,就更新区间最右值;如果找右边界,就更新区间最左值
- 时间复杂度 O ( log n ) O(\log n) O(logn)
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//二分查找 合并查找
return {getBoard(nums, target, "left"), getBoard(nums, target, "right")};
}
private:
//二分查找 合并查找 寻找左右边界
int getBoard(vector<int>& nums, int target, const string& keywords)
{
int left = 0, right = nums.size()-1;
int result = -1;
while(left <= right)
{
int mid = left + (right - left) / 2;
//目标值在左区间 更新区间右值
if(nums[mid] > target) right = mid - 1;
//目标值在右区间 更新区间左值
else if(nums[mid] < target) left = mid + 1;
//找到目标值 确定左右边界
else
{
result = mid;//找到目标值
//根据字段 更新区间边界值
if(keywords == "left") right = mid - 1;//左边界 更新区间的最右值
else if(keywords == "right") left = mid + 1;//右区间 更新区间左值
else cout << "invalid keywords!" <<endl;
}
}
return result;
}
};
二分查找 合并查找 写法2
- 二分查找,找到目标值,并返回nums数组其中一个值为目标值的下标。如果没有找到,返回-1
- 左右指针滑动找到要求的区间,左指针,向左滑动,找左边界;右指针,向右滑动,找右边界
- 时间复杂度 O ( log n ) O(\log n) O(logn)
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//二分查找 合并查找 写法2
int index = getBoard2(nums, target);
if(index == -1) return {-1, -1};// nums 中不存在 target
int leftboard = index, rightboard = index;
//向左滑动,找左边界
while((leftboard - 1) >= 0 && nums[leftboard - 1] == target) leftboard--;
//向右滑动,找右边界
while((rightboard + 1) < nums.size() && nums[rightboard + 1] == target) rightboard++;
return {leftboard, rightboard};
}
private:
//二分查找 合并查找 寻找左右边界 写法2
int getBoard2(vector<int>& nums, int target)
{
int left = 0, right = nums.size()-1;
while(left <= right)
{
int mid = left + (right - left) / 2;
//目标值在左区间 更新区间右值 右侧逼近
if(nums[mid] > target) right = mid - 1;
//目标值在右区间 更新区间左值 左侧逼近
else if(nums[mid] < target) left = mid + 1;
else return mid;//找到目标值
}
return -1;
}
};
922. 按奇偶排序数组II
写法1 辅助数组
时间复杂度、空间复杂度: O ( n ) O(n) O(n)
class Solution {
public:
vector<int> sortArrayByParityII(vector<int>& nums) {
//写法1 辅助数组
int oddindex = 1, evenindex = 0;
vector<int> result(nums.size());
for(int i=0; i<nums.size(); i++)
{
if(nums[i] % 2 == 0)
{
result[evenindex] = nums[i];
evenindex += 2;
}
else
{
result[oddindex] = nums[i];
oddindex += 2;
}
}
return result;
}
};
写法2 没有辅助数组
时间复杂度: O ( n ) O(n) O(n),空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public:
vector<int> sortArrayByParityII(vector<int>& nums) {
//写法2 没有辅助数组
int oddindex = 1;
for(int evenindex=0; evenindex<nums.size(); evenindex += 2)
{
//偶数位上的数字是奇数
if(nums[evenindex] % 2 == 1)//偶数位 遇到奇数
{
//奇数位找一个偶数进行交换位置 奇数下标要更新
//通过while找到奇数位上的偶数,奇数进入while循环,偶数不进入
while(nums[oddindex] % 2 ==1) oddindex += 2;
swap(nums[evenindex], nums[oddindex]);
}
}
return nums;
}
};
35.搜索插入位置
- 四种情况,left / right左右指针记录位置
- 目标值在数组左边,找到的区间是[0, -1],插入位置是0,right+1 / right
- 目标值在数组右边,找到区间是[0, nums.size() - 1],插入值位置是nums.size() - 1,right+1 / right
- 目标值在数组中但不存在,找到区间是[left, right],插入位置是right + 1 / right
- 目标值在数组中且存在,找到位置mid
二分查找 左闭右闭写法
返回mid或者right+1,时间复杂度: O ( l o g n ) O(log n) O(logn)
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
//二分查找 左闭右闭区间写法
int left = 0, right = nums.size() - 1;
while(left <= right)
{
int mid = left + (right - left) / 2;
if(nums[mid] > target) right = mid - 1;
else if(nums[mid] < target) left = mid + 1;
else return mid;//目标值在数组中且存在
}
return right + 1;
}
};
二分查找 左闭右开区间写法
返回mid或者right,时间复杂度: O ( l o g n ) O(log n) O(logn)
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
//二分查找 左闭右开区间写法 right mid
int left = 0, right = nums.size();
while(left < right)
{
int mid = left + ((right - left) >> 1);
if(nums[mid] > target) right = mid;
else if(nums[mid] < target) left = mid + 1;
else return mid;
}
return right;
}
};