目录
前情理论基础
复杂度
时间复杂度:数据规模有关,大O法默认数据规模足够大
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)
递归时间复杂度=递归次数 * 每次递归中操作的次数(如一次乘法操作)
eg: 如下实现求x的n次方的递归算法代码O(logn)。仅有一个递归调用,每次n/2,递归了logn次(以2为底)。每次递归做了一次乘法操作。
int function4(int x, int n) {
if (n == 0) return 1;
if (n == 1) return x;
int t = function4(x, n / 2);// 把这个递归操作抽取出来
if (n % 2 == 1) {
return t * t * x;
}
return t * t;
}
空间复杂度:程序运行时占用内存大小,而不是可执行文件大小。
只是一些普通变量就是o(1),开辟了数组o(n).
递归空间复杂度=每次递归空间复杂度 * 递归的深度
eg:如下实现斐波那契数列的二分法递归算法o(logn)。归深度logn,每次递归都是公用一块数组地址空间(!!!c++中函数传递数组参数,只是传入了数组元素首地址,并不是将整个数组拷贝一份传了进去),为o(1)。
int binary_search( int arr[], int l, int r, int x) {
if (r >= l) {
int mid = l + (r - l) / 2;
if (arr[mid] == x)
return mid;
if (arr[mid] > x)
return binary_search(arr, l, mid - 1, x);
return binary_search(arr, mid + 1, r, x);
}
return -1;
}
数组
数组是连续性存储,其元素不能被删除,只能被覆盖(!!!使用erase库时,即vector的size()来进行自减操作,只是计算了数组的逻辑大小而已,物理上某些元素依然存在,eg:27移除元素)
vector是容器,底层实现是数组
二分法:有序,无重复(若有重复,则返回值不唯一,eg:34在排序数组中查找元素的第一个和最后一个位置)
704二分查找
关键就是区间的开闭和“=”的使用与否。左闭右开与左闭右闭,严格根据区间定义来做边界处理,保证当前区间合法,可明确当前所在区间。个人感觉双闭区间好理解和使用一些,要取等都取等,要加或减1就都相应操作。
ps: int mid = left + ((right - left) / 2) 等同于int mid = (left + right) / 2,这样做可以防止两个大整数相加导致移除,使得其仅依赖于两者之间的差异而不是绝对值
class Solution {
public:
int search(vector<int>& nums, int target) {
// 左闭右闭
int left = 0;
int right = nums.size() - 1; // 左闭右闭,最右边取得到
while(left <= right) { // left=right在这个区间是合法的,让这个区间值取完
int mid = (left + right) / 2;
if(nums[mid] < target) { // target在右区间,nums[mid]已经小于了,根据左闭,不能再取到他,所以mid+1
left = mid + 1;
} else if(nums[mid] > target) { // 左区间,右闭,不能再取到他,所以mid-1
right = mid - 1;
}else {
return mid;
}
}
return -1;
}
};
class Solution {
public:
int search(vector<int>& nums, int target) {
// 左闭右开
int right = nums.size(); // 左闭右开,右边取不到
while (left < right) {// 右开,要合法,left不会等于right
int mid = left + ((right - left) >> 1); // 位移>>1,防止溢出,这样可以只依赖left,right的差异
if (nums[mid] < target) { // 左闭,不能再取到mid
left = mid + 1;
}else if (nums[mid] > target) { // 右开,本身就不会取得到Mid
right = mid;
}else {
return mid;
}
}
return -1;
}
};
时间复杂度:O(logn)
空间复杂度:O(1)
35搜索插入位置
题目关键词:升序即有序,时间复杂度o(logn),想到用二分法来解决。
这里主要是要先想清楚可能的情况
- 目标值在数组所有元素之前
- 目标值等于数组中某一个元素
- 目标值插入数组中的位置
- 目标值在数组所有元素之后
与二分法相比,就是多了个即使不存在也要返回索引值的步骤,这里最神奇的就是除了等于这一情况外其他三种情况一行代码就解决了,这里需要自行手写模拟一遍情况最好,然后想清楚每种情况时的区间,确定返回值
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效
int middle = left + ((right - left) / 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;
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0, -1]
// 目标值等于数组中某一个元素 return middle;
// 目标值插入数组中的位置 [left, right],return right + 1
// 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1
return right + 1;
}
};
34在排序数组中查找元素的第一个和最后一个位置
有重复,返回多值,二分找左边界,找右边界,先别想着一来就一次性找两个边界,理不清楚。
依然先分析可能情况
- 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
- 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
- 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
这里找边界要理解清楚,所找的边界是不包含target的。找右边界,当来到右区间,找最后一个可能是目标值的位置,此时只知道在右区间,更新left,用rightBorder来记录可能是最后一个位置的后一个位置,比如nums[mid]=target时(在左区间时,左边界找到第一个可能是目标值的位置)
(这道题的边界处理还得好好理解)
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
//情况一 目标值在数组范围左边或者右边
if (leftBorder == -2 || rightBorder == -2) return {-1, -1}; //{}返回数组
//情况三 目标值在数组范围中,且数组中有目标值
if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1}; // 边界间含有target,同时因为找到的边界是不包含target的,故加一减一
//情况二 目标值在数组范围中,但数组中无目标值
return {-1, -1};
}
private:
// 找右边界(不包括target),[3,3]找1
int getRightBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) { //左闭右闭,left=right在其中,合法
int mid = left + ((right - left) / 2);
if (nums[mid] > target) {
right = mid - 1;
} else { //找右边界,即最后一个可能是目标值的位置,这个时候只知道目标值在右边,但不知道具体在哪里,当nums[middle]=target时,同样更新left,以rightborder来保存这个可能的位置
left = mid + 1;
rightBorder = left;
}
}
return rightBorder;
}
// 找左边界,同理
int getLeftBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int leftBorder = -2;
while (left <= right) {
int mid = left + ((right - left) / 2); // 别忘记写了!
if (nums[mid] < target) {
left = mid + 1;
}else { // target在左区间,更新right,leftBorder来记录可能找到位置的前一个,在nums[middle] == target的时候更新right
right = mid - 1;
leftBorder = right;
}
}
return leftBorder;
}
};
27移除元素
暴力解法就是两个for循环,时间复杂度O(n^2)。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 暴力解法,时间复杂度n^2,空间复杂度1
int length = nums.size(); // 数组大小不需要-1
for(int i = 0; i < length; i++) {
if (nums[i] == val) {
for (int j = i; j < length - 1; j++) {
nums[j] = nums[j + 1]; // 这样写要注意j + 1不能超索引范围,故for里j<length-1。所以最好还是for(int j = i +1;j<length;j++) nums[j-1] = nums[j] 直接让j被限制
}
length--; //数组大小-1
i--; //保证后面新移动到i位置的元素在下一轮循环中也能遍历到。下标i以后的数值都向前移动了一位,i也向前移动一位
}
}
return length;
}
};
主要还是双指针法(时间复杂度O(n)):
(主要用于数组,链表,字符串中)
(这里就将前面提到的数组只覆盖不删除给深刻体现了。这样做,有可能val值并没有被移除完,但已符合逻辑长度且前面的元素都已不是val值的元素,这里就和题目中提到的“不需要考虑数组中超出新长度后面的元素”相对应了,一环扣一环啊!)
快慢指针(一个for循环实现暴力解法两个for循环的操作)
本质:快指针来寻找新数组中不是val值的元素
慢指针来记录新数组下标的位置。
slow必是指向新数组元素的后一位,再加上数组下标从0开始,故直接返回slow
// 快慢指针
int slow = 0;
int size = nums.size();
for (int fast = 0; fast < size; fast++) {
if (nums[fast] != val) { // 快指针获取纳入新数组的元素
nums[slow++] = nums[fast]; // 慢指针更新下标
}
}
return slow; // 数组下标从0开始,此时slow值即是新数组大小
相向双指针(改变了元素的相对顺序,但移动元素少)
本质:左边找等于val的元素,右边找不等于val的元素,然后右边找到的覆盖左边找到的。
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
while (leftIndex <= rightIndex && nums[leftIndex] != val) { // 左指针找等于val的
++leftIndex;
}
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
--rightIndex;
}// 右指针找不等于val的
if (leftIndex < rightIndex) { // 用右边不等于val的元素去覆盖左边等于val的
nums[leftIndex++] = nums[rightIndex--];
}
// 虽然可能还有元素没被覆盖,但他已不在逻辑长度内了,这里对应题目“不考虑数组中超出新长度后面的元素”
}
return leftIndex; // 指向了最终数组末尾的下一个元素(++了),又由于索引从0开始
欧克,记录完毕。主要还是理解得很慢,一个点没想通能想好久。下次注意及时寻找帮助,别光那硬嗑。试试边刷边写博客,这样搞完了再来单独写一遍博客虽然又梳理了一遍但有点费时了,赶进度路漫漫啊。。。。虽然码龄两年,但这倒还是第一篇博客。。