文章目录
终于开始刷算法了。。
![在这里插入图片描述](https://img-blog.csdnimg.cn/591d290341b84252934e4ccfa6cc8a58.jpeg#pic_center)
一、数组的理论基础
总结:
- 数组是存放在连续内存空间上相同类型数据的集合
- vector和array的区别
- c++中二维数组在地址空间上是连续的
1、理论基础
(1)数组下标都是从0开始的。
(2)数组内存空间的地址是连续的
问题:那么二维数组在内存的空间地址是连续的么?
不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。
我们来做一个实验,C++测试代码如下:
void test_arr() {
int array[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}
int main() {
test_arr();
}
测试地址为
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834
注意地址为16进制,可以看出二维数组地址是连续一条线的。
一些录友可能看不懂内存地址,我就简单介绍一下, 0x7ffee4065820 与 0x7ffee4065824 差了一个4,就是4个字节,因为这是一个int型的数组,所以两个相邻数组元素地址差4个字节。
0x7ffee4065828 与 0x7ffee406582c 也是差了4个字节,在16进制里8 + 4 = c,c就是12。
拓展:java二维数组在内存的空间地址是连续的?
像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。
所以看不到每个元素的地址情况,这里我以Java为例,也做一个实验。
public static void test_arr() {
int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
}
输出的地址为:
[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05
这里的数值也是16进制,这不是真正的地址,而是经过处理过后的数值了,我们也可以看出,二维数组的每一行头结点的地址是没有规则的,更谈不上连续。
2、vector 与 array
如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
关于 vector 与 array 的异同,内容比较多,这里不做详细叙述,下面仅列出部分:
-
创建方式上不同
-
- vector无需指定大小,只需指定类型,如 vector a。
- array需指定类型和大小,如 array<int, 3> a。
-
内存使用上不同
-
- vector需要占据比array更多的内存,因为其内存空间大小是动态可变的。
- array内存高效的,用多少就申请多少。
-
效率上不同
-
- vector效率偏低,因为当向vector中添加新元素的时候,内存空间不够,需要重新申请更大的空间,由于vector是连续内存空间的,因此其申请更多空间的时候,可能整个位置发生改变,需要将原来空间里的数据拷贝过去。
-
swap操作不同
-
- vector是将引用进行交换,效率高,其迭代器指向原来的容器(原来的容器中的元素指向的却是另一个容器的值),但是end的引用并没有发生交换,因此在输出的时候注意别用end作为迭代终止条件。
- array是进行值的交换,效率低,且迭代器仍指向原来的容器。
二、二分查找
1、题意
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
2、解题思路
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right)
还是 while(left <= right)
,到底是right = middle
呢,还是要right = middle - 1
呢?
二分法的写法因区间的定义不同,因而有两种写法:
- 左闭右闭即[left, right]
- 左闭右开即[left, right)
2.1 左闭右闭 [left, right]
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while(left <= right){ // 采用<= :因为是[left,right]闭区间,所以left = right时,仍有效
int middle = left + (right - left)/2; // 防溢出
if(nums[middle] < target){
left = middle + 1;
}
else if(nums[middle] > target){
right = middle - 1;
}
else{
return middle;
}
}
return -1; // 这个要记住,未找到target,则return -1 而不是 None
}
};
2.2 左闭右开 [left, right)
class Solution {
public:
int search(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){
left = middle + 1;
}
else if(nums[middle] > target){
right = middle;
}
else{
return middle;
}
}
return -1;
}
};
PS:" mid = ( left + right ) >> 1 "中的 “>>” 是什么意思?
右移运算符>>,运算结果正好能对应一个整数的二分之一值,这就正好能代替数学上的除2运算,但是比除2运算要快。原式相当于mid=(left+right)/2
三、移除元素
1、题意
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
2、解题思路
2.1 暴力法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
PS:错误写法
错误关键点:记录数组有效长度不是从-1开始向上加,而是从num.size()开始向下减少,注意for循环条件:不是写成 i<nums.size() -1,而是i < size,size这个变量是不断变化的
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int result = -1;
for(int i=0; i<nums.size(); i++){
if(nums[i] == val){
for(int j = i+1; j < nums.size(); j++){
nums[j-1] = nums[j];
}
i--;
continue;
}
result++;
}
return result;
}
};
2.2 双指针法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0; // 初始值为0,而不是-1
for(int fast = 0; fast < nums.size(); fast++){
if(nums[fast] != val){
nums[slow++] = nums[fast];
}
}
return slow;
}
};
拓展:
/**
* 相向双指针方法,基于元素顺序可以改变的题目描述改变了元素相对位置,确保了移动最少元素
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
// 找左边等于val的元素
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一定指向了最终数组末尾的下一个元素
}
};
四、有序数组的平方
1、题意
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
2、解题思路
2.1 暴力法
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for(int i=0; i<nums.size();i++){
nums[i] = nums[i] * nums[i];
}
sort(nums.begin(), nums.end()); // 快速排序
return nums;
}
};
PS:超时
for(int i=0; i< nums.size();i++){
for(int j=i+1; j<nums.size();j++){
int temp;
if(nums[i] > nums[j]){
temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
}
2.2 双指针法
思路:平方后,我们知道,对于这个数组,从两边向中间移动,大小逐渐减小,那么我们就可以用一个指针指向数组开头,另一个指向数组末尾,不断取最大值放到一个新数组里,知道两个指针相遇(遍历结束)。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result(nums.size(), 0);
int index = nums.size() - 1;
for(int slow = 0, fast = nums.size() - 1;slow<=fast;){
if(nums[slow] * nums[slow] > nums[fast]*nums[fast]){
result[index] = nums[slow]*nums[slow];
slow++;
}
else{
result[index] = nums[fast] * nums[fast];
fast--;
}
index--;
}
return result;
}
};
五、长度最小的子数组
1、题意
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其和 ≥ target
的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度**。**如果不存在符合条件的子数组,返回 0
。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
2、解题思路
2.1 暴力法
注意这里要求的是“连续子数组”,满足条件是"大于等于"target,而不是"等于",不好好审题吃大亏。。。。
双重循环即可:
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
PS: 有个错误写法:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = nums.size() + 1;
for(int i = 0; i < nums.size(); i++){
int sum = nums[i];
for(int j = i + 1; j < nums.size(); j++){
if(sum < target){
sum += nums[j];
}
else{
int temp = j - i;
result = result < temp ? result:temp;
}
}
}
return result;
}
};
错误关键点就在if(sum < target){sum += nums[j];},这样会导致,比如数组最后两个值相加 >= target ,而用这种写法会导致循环终止,但result没有更新。
2.2 滑动窗口
滑动窗口可以用一个for循环来完成任务。
精髓在于:当累加达到target,就该把窗口起始位置向前移动了。
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
Q:为什么时间复杂度是O(n)。
A:不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
六、螺旋矩阵II
1、题意
给你一个正整数 n
,生成一个包含 1
到 n2
所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
。
2、解题思路
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
由外向内一圈一圈这么画下去。
3、示范代码
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};