数组-二分查找
给定一个 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]之间。
题上已知是有序数组且数组中无重复元素,这两个条件是使用二分查找的必要条件,说明我们可以考虑一下能不能用二分查找实现算法
注意:一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的 ,就不能使用二分查找了
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)/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 -1;
}
};
数组-移除元素
给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
暴力解法:
两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组.时间复杂度为O(n^2),空间复杂度为O(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;
}
};
双指针法:
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作,在数组和链表的操作中非常常见,很多考察数组、链表、字符串等操作的面试题,都使用双指针法
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
数组-有序数组的平方
给你一个按 非递减顺序 排序的整数数组 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]
暴力排序:每个数平方之后再排序 :
class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
for (int i = 0; i < A.size(); i++) {
A[i] *= A[i];
}
sort(A.begin(), A.end()); // 快速排序
return A;
}
};
这里用伪c语言说一下上述的快速排序方法sort()吧:
void QSort(SqList &L,int low,int high) { //对顺序表L快速排序
if (low < high) { //长度大于1
privotloc = Partition(L,low,high); //将L.[low...high]一分为二,privotloc为中轴元素排好序的位置
QSort(L,low,privotloc-1);//对低字标递归排序
QSort(L,privotloc+1,high);//对高子表递归排序
}
}
int Partition(SqList &L,int low,int high) {
L.r[0] = L.r[low];
privotkey = L.r[low].key;
while (low < high) {
//嵌套的这两个while循环的low<high是必不可少的,因为有可能这个嵌套的while跑着跑着low >= high
while (low < high && L.r[righ].key >= privotkey) --high;
L.r[low] = L.r[high];
while (low < high && L.r[righ].key <= privotkey) ++low;
L.r[high] = L.r[low];
}
L.r[low] = L.r[0];
return low;
}
Qsort()的时间复杂度:O(logn)
Partition:因为这个函数没调用一次所有函数就都和privotkey比较一次,故时间复杂度是O(n)
所以暴力排序总体时间复杂度是O(n+logn),可以说是O(nlogn)的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 O(n + nlogn)
双指针法:
class Solution {
public:
vector<int> sortedSquares(vector<int>& A) {
int k = A.size() - 1;
vector<int> result(A.size(), 0);//创建一个result数组,我起初认为题中没有说可以用额外的数组那就不能用,现在我认为只要题中没有不让干什么事那就可以干
for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素
if (A[i] * A[i] < A[j] * A[j]) {
result[k--] = A[j] * A[j];
j--;
}
else {
result[k--] = A[i] * A[i];
i++;
}
}
return result;
}
};
时间复杂度为O(n)
数组-长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组
暴力解法:时间复杂度O(n^2),空间复杂度O(1)
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;
}
};
滑动窗口:
- 其实滑动窗口也可以理解为双指针法的一种,只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更合适一些
- 窗口就是满足其和>=s的长度最小的连续子数组
- 窗口的起始位置如何移动:如果当前窗口的值大于s了,先要判断一下窗口能否缩小(就是窗口的左边界能否向右移动并且窗口仍>=s)
- 判断结果如果能缩小就循环缩小直到不能再缩小,然后窗口右边界右移,如果不能缩小就直接右边界右移
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;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
注意:不要以为for里放一个while就以为是O(n*n), 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是2 * n 也就是O(n)
螺旋矩阵II
给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3
输出:
[
[ 1, 2, 3 ],
[ 8, 9, 4 ],
[ 7, 6, 5 ]
]
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
由外向内一圈一圈这么画下去
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来
这里我用左闭右开的原则:
class Solution {
public int[][] generateMatrix(int n) {
int i=0,j=0;//数组的坐标,用来确定每次在哪个位置填充数字
int num=1;//定义填充数字
int m=1;//1.用来记录循环了几次,从而判断是否要继续while循环2.规定每一边画到哪个位置
int[][] a=new int[n][n];
while(m<n/2+1)//规定循环次数
{
while(j<n-m) a[i][j++]=num++;//规定每一边画到哪个位置
while(i<n-m) a[i++][j]=num++;
while(j>m-1) a[i][j--]=num++;
while(i>m-1) a[i--][j]=num++;
//这一圈画完后ia[i][j]定位到这一圈的起始位置,所以i,j需要自增
i++;
j++;
m++;
}
if(n%2==1) a[n/2][n/2]=n*n;//如果n为奇数则需要在中心点额外填充数字
return a;
}
}