人决定分类型来做,一开始是简单的数组以及其他,后期在涉及其他数据结构
数组
如果有说明数组内的数据均为0~n-1,那我们就可以把数组和下标联合起来,进行思考
例如:
剑指 Offer 03. 数组中重复的数字这道题里面的原地置换法
这题的条件就是从数组中找重复的数字,且数组内的数据均为0~n-1
三种解法:
第一种最容易想到的,暴力解法:
原理就是先利用sort对数组进行排序,然后重复的数字必然会挨在一起,以此为依据来查找
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
sort(nums.brgin() , nums.end());
for(int idx = 0;idx < nums.size()-1;idx++)
{
if(nums[idx] == nums[idx+1])
return nums[idx];
}
return -1;
}
};
第二种,以当前数组的数据建立哈希表,利用哈希表的高查找效率
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
unordered_map <int,int> map;
for(int num : nums)
{
if(++map[num] > 1) return num;
}
return -1;
}
};
第三种,巧妙的原地置换法
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int buf = 0;
for(int idx = 0;idx<nums.size();idx++)
{
while(nums[idx] != idx)
{
if(nums[idx] == nums[nums[idx]])
return nums[idx];
buf = nums[idx];//交换位置
nums[idx] = nums[nums[idx]];
nums[nums[idx]] = nums[idx];
}
}
return -1;
}
};
又例如
剑指 Offer 53 - II. 0~n-1中缺失的数字
这题的关键字是有序数组+查找,依照大佬们的思路,优先考虑二分查找
我们设置左、右、中间三个指针,又因为数组中每个数字都在范围0~n-1之内且只缺少了一个,所以也可以把下标和数据联系起来
class Solution {
public:
int missingNumber(vector<int>& nums) {
int L = 0, R = nums.size();
while(L < R)
{
int mid = L + ((R - L)>>1);//使用右移运算符比/2快
//如果中间指针的数据于下标对应,就表示缺的数字在右半边,
//于是我们把L往右挪,反之就把R往左挪
if(nums[mid] == mid)
//为什么L要+1?因为我们要把L移动到到缺失的数字对应的下标上,这就意味着
//L的位置是不可能nums[mid] == mid的,所以直接跳过mid,到下一位上去;
L = mid + 1;
else
R = mid;
}
return L;
}
};
另一个思路,并且适用于无序的情况
利用异或的性质
仔细观察,我们就能发现却一个数字的有序数组的共同点
除了一个“8”和缺少了数据的下标外,其他的数据和下标都是一一对应的,由此我们可以把所有的下标和数据进行异或运算
进行运算后,我们就只剩下想要得数字与那个“8”,而这个“8”,其实就是数组长度,因为数据和下标都是从0开始且有序,所以只要缺少一个,最后的数据必然是“8” ,我们只要再^nums.size(),就可以把多余的“8”去掉;
int missingNumber(vector<int>& nums)
{
int size = nums.size(),
int data = 0;
for (int idx = 0; idx < size; idx++)
data ^= nums[idx] ^ idx;
return data ^ size;
}
此方法思路简单,易理解,但是时间复杂度是O(N),没有利用好有序这个条件
这里还有一题可以用到异或的题目:
剑指 Offer 56 - I. 数组中数字出现的次数
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
因为空间复杂度要求是O(1),所以用不了哈希表了,可以考虑以下异或
如果只有一个数字的话,直接全异或就行了,这里有两个数字,我们异或的话,最后会得到num1^num2,
咋把他们分开呢?直接分开或许是做不到,但是我们可以把数组分组,一边包含num1于其他成对的数字,另一边是num2与剩下的成对的数字
原理:0 ^ 0 = 0 、1 ^ 1 = 0 、 但是0 ^ 1 = 1,于是我们找到num1^num2里为1的任意一位,这一位就就是区分开num1 和 num2 重要线索,其他的元素因为是成对的,所以用这一位就能把他们分到同一组
代码:
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
int res = 0 , num1 = 0 , num2 = 0;
for(int n : nums)//求得num1 ^ num2的值
{
res ^= n;
}
int idx = 1;
while(!(idx & res))
{
idx = idx << 1;//求得res里为1的最低位
}
for(int n : nums)
{
if((n&idx) == 1)//分组并分别异或
num1 ^= n;
else
num2 ^= n;
}
return vector<int> {num1,num2};//返回数组
}
};
利用改进的双指针法实现有序数组中删除重复项这类题目的通解
80. 删除有序数组中的重复项 II
这题要求删除重复数据,使得一个重复的数据最多出现两次,因此数组长度<=2时都是特殊情况
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int i = 0;//定义指针1
for(num : nums)//这里的num就相当于第二个指针
{//将此处两个2改为其他数字即可应对不同题目
if(i<2 || nums[i-2] != num)
{
//i<2是特殊情况,而整个判断条件可以理解为,遇到重复值的时候,
//i就不往前走,因为i是代表最后的数组长度,重复的数据是不能被算入长度内的
//而覆盖重复数据的操作就通过num来完成,当num到达不重复的数据的时候
//就用num的数据把i所在的数据给覆盖掉,然后长度加1,也就是i++
//这样就保证了在i的范围内,数据都是满足要求的,
nums[i++] = num;
}
}
return i;
}
};
26. 删除有序数组中的重复项
同上的通用解法:
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int idx = 0;
for(int num : nums)
{
if(idx<1 || nums[idx-1] != num)
nums[idx++] = num;
}
return idx;
}
};
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
来源:力扣(LeetCode)
思路:
原地删除,双指针就完事了
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int res = 0;
for(int num : nums)
{
//只有当前值不时要删除的值时,才进行替换,如果是要删除的值,
//就跳过,啥也不做,等到再次遇到正常值时,再把正常值替换回去,
//为此,res不能乱走,只有确定了当前值正常的时候才能往前走
if(num != val)
{
nums[res] = num;
res++;
}
}
return res;
}
};
有序旋转数组的最小值(无重复值)
153. 寻找旋转排序数组中的最小值
也是典型的二分法
class Solution {
public:
int findMin(vector<int>& nums) {
int N = nums.size();
int L = 0, R = N ;
while(L < R)
{
int mid = L + ((R - L)>>1);
if(nums[mid]>=nums[0])
L = mid + 1;
else
R = mid;
}
if(L >= N) return nums[0]
return nums[L] < nums[0] ? nums[L] : nums[0];
}
};
有重复值
154. 寻找旋转排序数组中的最小值 II
有重复值的题目其实只多了一个while语句,这一步是恢复数组二段性得重要步骤
class Solution {
public:
int findMin(vector<int>& nums) {
int N = nums.size();
int L = 0 , R = N - 1;
while(L < R && nums[0] == nums[R])//恢复二段性
{
R--;
}
R++;//左闭右开
while(L < R)
{
int mid = L + ((R - L)>>1);
if(nums[mid]>=nums[0])
L = mid + 1;
else
R = mid;
}
if(L >= N) return nums[0]
return nums[L] < nums[0] ? nums[L] : nums[0];
}
};
二维数组
剑指 Offer 04. 二维数组中的查找
这一题是一个上下,左右都有序(递增)的二维数组,要判断输入参数target是否在数组内,因为是有序的,所以我把它当成坐标轴来看待,从左下或右上去逼近于参数最相近的范围
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
int x = 0, y = matrix.size()-1;//我选择从左下角开始
//设置循环条件,当索引越界时就说明找不到该值,只能返回false了
while(y >= 0 && x < matrix[0].size() )
{
if(matrix[y][x] > target) y--;
else if(matrix[y][x] < target) x++;
else return true;
}
return false;
}
};
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size();
if(m == 0) return {};
//这里的n一定要在判断之后定义,不然如果是空数组的话就会出现无效指针,
//导致程序崩溃
int n = matrix[0].size();
vector <int> res;
res.reserve(m*n);
int y = 0, x = 0, yup = 0,ydown = m, xleft = -1, xright = n;
int state = 1;
while(1)
{
switch(state)
{
case 1:
while(x<xright)
{
res.emplace_back(matrix[y][x]);
x++;
}
x--;
y++;
xright--;
state = 2;
break;
case 2:
while(y<ydown)
{
res.emplace_back(matrix[y][x]);
y++;
}
y--;
x--;
ydown--;
state = 3;
break;
case 3:
while(x>xleft)
{
res.emplace_back(matrix[y][x]);
x--;
}
x++;
y--;
xleft++;
state = 4;
break;
case 4:
while(y>yup)
{
res.emplace_back(matrix[y][x]);
y--;
}
y++;
x++;
yup++;
state = 1;
break;
default:
break;
}
if((state == 1 || state == 3) && yup >= ydown) break;
if((state == 2 || state == 4) && xleft >= xright) break;
}
return res;
}
};
还有一题可以用这个套路:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n);
for(vector<int>& temp : res)
{
temp.resize(n);
}
int xright = n, xleft = -1, ydown = n, yup = 0;
int y = 0, x = 0;
int state = 1, num = 1;
while(num<=(n*n))
{
switch(state)
{
case 1:
while(x<xright)
{
res[y][x] = num;
x++;num++;
}
++y;--x;--xright;state = 2;
break;
case 2:
while(y<ydown)
{
res[y][x] = num;
y++;num++;
}
--y;--x;--ydown;state = 3;
break;
case 3:
while(x>xleft)
{
res[y][x] = num;
x--;num++;
}
--y;++x;++xleft;state = 4;
break;
case 4:
while(y > yup)
{
res[y][x] = num;
y--;num++;
}
++y;++x;++yup;state = 1;
break;
default : break;
}
}
return res;
}
};
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
来源:力扣(LeetCode)
思路:
遍历数组的同时把元素放入哈希表,一旦查到 target - nums[ i ]存在哈希表里,就返回这两个数字,遍历完了都没找到的话,就说明不存在这样的一对数字
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int ,int>map;
for(int i =0;i<nums.size();i++)
{//判断如果两个数字都在哈希表里,就直接返回下标,
//下标储存在map里的value里
if(map.find(target-nums[i])!=map.end())
return vector<int>{i,map[target-nums[i]]}//返回
map[nums[i]] = i;//key是数字,value是下标;
}
return {};
}
};
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
来源:力扣(LeetCode)
第一眼看见题目,俩有序数组,还中位数?归并排序然后直接中间位置走起,于是刷刷写下如下代码
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int n1 = nums1.size() , n2 = nums2.size();
vector<int> buf(n1+n2,0);
int p1 = 0 , p2 = 0;
double res;
for(int i = 0;i<n1+n2;i++)
{
int num1 = p1<n1?nums1[p1] : INT_MAX;
int num2 = p2<n2?nums2[p2] : INT_MAX;
if(num1 < num2)
{
buf[i] = num1;p1++;
}
else
{
buf[i] = num2;p2++;
}
}
if((n1+n2)%2 == 0)
res = ((double)buf[((n1+n2)/2)-1] + (double)buf[(n1+n2)/2])/2;
else res = (double)buf[(n1+n2)/2];
return res;
}
};
写完调试完,提交,好像还不错😘
然后我才发现有进阶要求:
进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?
呃呃,有点难😩,待续…
剑指 Offer 51. 数组中的逆序对
第一眼,没啥想法,暴力法试试吧,然后:😢
暂时没啥头绪,先搁置吧…😥
时隔不知道多少天,终于搞懂了,这一题可以利用归并排序的特点:分而治之
写出归并排序的模板,再添加一行代码即可
模板👉在这
思路:
其实也就只需解释一下那行代码而已,不过前提是需要理解归并排序
为什么在nums[left1] > nums[left2] 时?为什么是mid-left1+1?
nums[left1] > nums[left2] 时代表左半部分当前遍历到的值比右半部分当前遍历到的值大,
于是!!! 此时nums[left1] 与 nums[left2]就组成了一组逆序对,
但是!!! 别忘了,合并的两个子数组又是有序的!!,所以nums[left1]到nums[mid]之间的所有数都比nums[left2]大,他们都能组成逆序对!!
mid-left1+1就是这个原理啦
最后因为归并排序的原理,从下往上分割合并,所以在用上述方法判断逆序对时,因为两个子数组里的元素相对位置不会错,所以是正确的!而且也不会漏判
代码:
class Solution {
int count;//计数值
void Merge(vector<int>& nums, int L,int mid, int R)
{
if(L > R)return;
vector<int>temp(R-L+1);
int left1 = L, left2 = mid+1, idx = 0;
while(left1<=mid && left2<=R)
{
if(nums[left1]<=nums[left2])
{
temp[idx++] = nums[left1++];
}
else
{
count += mid-left1+1;//只需添加这一行即可
temp[idx++] = nums[left2++];
}
}
while(left1<=mid) temp[idx++] = nums[left1++];;
while(left2<=R) temp[idx++] = nums[left2++];;
for(int i = 0;i<temp.size();++i)
nums[L+i] = temp[i];
}
void MSort(vector<int>& nums, int L, int R)
{
if(L >= R) return;
int mid = L+((R-L)>>1);
MSort(nums, L, mid);
MSort(nums, mid+1, R);
Merge(nums, L, mid, R);
}
public:
int reversePairs(vector<int>& nums) {
count = 0;
MSort(nums, 0, nums.size()-1);
return count;
}
};
很简单的一题,注意一下二维数组的初始化就行了
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> res;
res.resize(numRows);
for(int i = 0;i<numRows;++i)//分别给每一行赋值
{
res[i].resize(i+1);//初始化各行的size
//给首位和末位填上1,如果对首位和末位用下面的公式的话就会越界,
//无奈只能提前初始化
res[i][0] = res[i][i] = 1;
for(int j = 1;j<i;++j)
{
res[i][j] = res[i-1][j-1] + res[i-1][j];
}
}
return res;
}
};
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
来源:力扣(LeetCode)
先来个最简单的暴力解法,只是一种思路,并不满足题目要求
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
vector<vector<int>> res = matrix;
int row = matrix.size();
for(int y = 0;y<row;++y)
{
for(int x = 0;x<row;++x)
{
res[x][row-y-1] = matrix[y][x];
}
}
matrix = res;
}
};
翻转法:
先上下翻转,再沿对角线翻转
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int row = matrix.size();
for(int i = 0;i<row/2;++i)
{//上下翻转
for(int j = 0;j<row;++j)
swap(matrix[i][j],matrix[row-i-1][j]);
}
for(int i = 0;i<row;++i)
{//对角线翻转
for(int j = 0;j<i;++j)
swap(matrix[i][j],matrix[j][i]);
}
}
};
对角线翻转的思路:
数字代表代码中翻转的顺序
31. 下一个排列
实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
来源:力扣(LeetCode)
这其实就是字典的排序方法,假如是{ 1 , 2 , 3}
按照升序排列就如下
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
思路:要求的不只是比当前的大的排列,而是要求下一个排列,就是要在大的排列里面找到最小的那一个,根据排序的规则,就是尽量不要改变前面,尽量改变最后面的顺序,这样才能最小的增大
那么怎么找到该改哪个呢?
看图,黑色是原序列,红色是对应的下一个排列,我们可以发现,当最后n位是降序的话,根本无法在这n位里改变顺序,不管怎么排,都会变小,因为降序,最大的数字都在最前位了,只能往前找一个不满足降序的数字x,它就是突破口,因为它破坏了降序,所以后面的n位里肯定有比它大的,我们要做的就是找到比x大的值里面最小的那个,交换他们两个即可
然后重点来了:
我们前面的的事是为了什么,是为了能够在最小的限度内增大吧?而经过交换后,x的位置已经变大了,这就意味这不管我们怎么改后面的排序,新的排序都已经比原排列大了,于是为了秉持最小的增大,我们把x后面的数字按增序重新排列,这样即可在已经大于原排列的情况下得到最小的那个排列
代码:
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int N = nums.size();
int i = N-1, j = N-1;
while(i>0 && nums[i] <= nums[i-1]) --i;--i;//找到x,把i移动到x的下标处,
//当然最后如果不想多减一次,那就略微修改下面的代码即可
if(i >= 0)//这一步别忘了哦,假如原序列是最大排列(整体降序),这时候i为-1,
//如果继续执行交换,就越界了,
//当最大排列值,直接反转(或排序整个数组)即可
{
while(j>=0 && nums[j] <= nums[i]) --j;//找到比x大的最小值;
swap(nums[j],nums[i]);//交换
}
sort(nums.begin()+i+1,nums.end());//排序
}
};
这一题也可以用反转代替排序,因为反转的复杂度为O(n),比排序的O(n log n)要好,这里我偷懒就不写了
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
进阶:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
来源:力扣(LeetCode)
方法一:
额外数组,很明显不满足进阶要求,但不失为一种解法
代码:
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int size = nums.size();
int L = 0, R = k%size;
if(R == 0)return ;
vector<int>buf = nums;
for(int i = 0;i<size;++i)
{
buf[R++] = nums[L++];
if(R == size)R = 0;
if(L == size)L = 0;
}
nums = buf;
return ;
}
};
方法二:
翻转数组法,没想到旋转和翻转这么配啊,上面有一题二维数组的旋转,也是用翻转实现的
先全局翻转一次,然后吧翻转过得数组在分两部分翻转,分别是前k个,和剩余部分
代码: 极简三行… 实在不行就自己写一个翻转得程序,也不难…
因为C++得reverse是迭代器+开区间的,所以直接k%size就行了,不用管什么+1,-1等区间问题
class Solution {
public:
void rotate(vector<int>& nums, int k) {
reverse(nums.begin(),nums.end());
reverse(nums.begin(),nums.begin()+k%nums.size());
reverse(nums.begin()+k%nums.size(),nums.end());
}
};