2022-12-27阳了,2023-01-01重新启动
1.数组的理论基础
数组本身就是一种线性表。
线性表:逻辑结构指的是1相同特征元素的2有序3序列。
线性表:存储结构包括顺序存储和链式存储,其中顺序存储指的就是一维数组,而链式结构就是链表。
数组在内存中的存储特点:
- 数组的下标都是从0开始的;
- 数组内存空间的地址是连续的;
2.二分查找
查找算法
首先,查找是数据结构与算法中的一大部分内容,在做这个题之前我们先简要把一些查找的方法总结一下;
对于一个散列表(哈希表),我们通过构建Hash函数后对元素进行查找的时间复杂度就是O(1)。具体内容可以参考数据结构与算法三【哈希表】
顺序查找
对于顺序表(顺序存储和链式存储),如果是顺序查找那么对于长度为n的顺序表,查找成功的平均时间复杂度就是 A S L 1 = 1 n × n ( n + 1 ) 2 ASL_1=\frac{1}{n} \times\frac{n(n+1)}{2} ASL1=n1×2n(n+1),其时间复杂度为O(n)。
对于查找失败的平均查找长度 A S L 2 = 1 n × n × n ASL_2=\frac{1}{n} \times n\times n ASL2=n1×n×n
折半查找(二分查找)
对于折半查找(二分查找)要求线性表本身有序,且题目一般要求无重复,因为重复的话返回的下标不唯一。
其平均查找长度为$ lg2(n+1)上取整-1$。时间复杂度是O(log_2(n))
编程实现
https://leetcode.cn/problems/binary-search/
其核心要点在于清晰的条件判定区间(这一不变量),在二分查找中要保持不变量。就是在while寻找每一次边界的时候都要根据区间的定义来操作,这就是循环不变量规则。
我们定义判定区间是一个[左,右]的闭区间,这样每次折半的时候,while判定条件中用<=或者>=,==号有意义,
分支判定,如果if(num[middle]>target) right就要赋值为num[middle-1],
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;
}else if(nums[middle]<target){
left = middle+1;
}else{
return middle;
}
}
return -1;
}
};
在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则!!
3删除元素
https://leetcode.cn/problems/remove-element/submissions/
暴力双循环思路:
时间复杂度是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; j<size-1;j++){
nums[j]=nums[j+1];
}
i--;
size--;
}
}
return size;
}
};
比较有技巧的方法就是快慢双指针法,
时间复杂度是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;//slowindex==新数组长度
}
};
4排序
最直观的想法,计算出平方后进行排序;或者边计算边进行插入排序。
二者本质上没有区别,先计算出来后,也需要逐个进行排序。
这里我们直接调用快速排序的包。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for (int i=0;i<nums.size();i++){
nums[i]*=nums[i];
}
sort(nums.begin(),nums.end());
return nums;
}
};
巧法:由于平方后的最大值一定在左右两侧,因此可以通过双指针法,设置左右两个指针,然后开辟一个和输入数组大小相等的数组,通过左右指针循环找最大值逆序填充到新开辟的数组里即可。
4.1各类排序算法分类总结
其中,在这个题目中,我们使用了C++ STL中的快速排序sort()函数,那还有哪些排序算法,不同的排序算法的时间复杂度是多少呢?
排序算分类总结:
插入类:
直折希、
交换类:
冒快、
选择类:
简堆、
归并和基数排序:
基归(基数排序和归并排序)
4.2排序算法时间复杂度分析
时间复杂度
基数排序比较特殊,平均时间复杂度为O(d(n+rd)) 空间负载度O(rd)
-
平均时间复杂度
口诀:快些以 n l o g 2 n nlog_{2}{n} nlog2n的速度归堆
(快速排序、希尔排序、归并排序、堆排序)
其他算法的平均时间复杂度都是O(n^2) -
最坏情况下的时间复杂度
快速排序在有序的时候要变慢O(n^2),其他算法最坏情况都和平均时间复杂度相同) -
最好情况下的时间复杂度
直接插入冒泡冒泡,有序时要变快O(n)
空间复杂度:
快归基本要求高
快速排序O(log2n); 归并排序O(n)、基数排序O(rd)
排序算法C++代码实现
插入类
直接插入排序
假设第一个元素已经有序,从第二个元素开始往有序序列中(从有序序列中,从后往前,依次比较插入)
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
for (int i=0;i<nums.size();i++){
nums[i]*=nums[i];
}
// sort(nums.begin(),nums.end());
int temp,k;
for (int j=1;j<nums.size();j++){
temp = nums[j];
k=j;
while(k>=1&&temp<nums[k-1]){
nums[k]=nums[k-1];
k--;
}
nums[k]=temp;
}
return nums;
}
};
数组操作-滑动窗口
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
其本质上是双指针法的一种,由于这种解法更像是一个窗口的移动,因此叫做滑动窗口更为合适。
在本题中,实现滑动窗口主要确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
窗口就是,满足其和>=s的长度最小的连续子数组。