适合使用索引进行查询,删除和增加操作少的场景。
下面举例都假定数组元素存储为int类型。
int arr[N] // N应为常量表达式的值或者常量,例如下面的示例
constexpr unsigned long int func(int n) {
return (n <= 1)? n : func(n-1) + func(n-2);
}// const 或const 的
int arr[func(11)];//
arr[0];
for (int i = 0; i < N; ++i) {
...
}
for (auto i : arr) {
...
}
int arr[N1][N2][N3]; // 多维数组,以三维为例
int *arr[N]; //指针数组,一个数组,内容为int类型指针
int (*arr)[N]; // 数组指针,一个指针,指向arr数组的首地址
// 注意多维数组的范围for结合auto的用法
// 其他略
for (const auto& row: arr) { // 此处&不能省略。防止auto解释为指针
for (const auto& index : row) { //此处可以省略
cout << index;
}
}
#include
using namespace std;
#include
using namespace std;
array<int , 10> a; //定义一个arr,长度为10.
array<int, 3> a1{ {1, 2, 3} }
array<int, 3> a2{ 1, 2, 3} // 两种定义和初始化方式结果一样
array<int, 3> a3{ } // 初始化为0
array<int, 3> a4{1} // 第一个初始化为1,其他初始化为0
//接上文
//1. 迭代器
a.begin();a.end();//第一个元素,最后一个元素后的随机访问迭代器。
a.rbegin();a.rend();//最后一个元素,指向第一个元素前一个的随机访问迭代器。
a.cbegin();a.cend();//与第一行相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
a.crbegin();a.crend();//与第二行相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
//2. 容器元素数量相关
a.size(), a.max_size();//为初始化的空间大小
a.empty();// 判断数组是否为空
//3.元素操作
int n = 10, value = 11;
a.at(n),a[n];//返回n位置的引用,前者如果n不是有效范围,抛出out_of_range 异常;后者为无效操作(运行时返回一个未初始化的值,可能导致意想不到的错误)
a.front(),a.back(); //返回容器第一个元素/最后一个元素直接引用(注意空的array不适用)
a.data(); //返回容器首元素指针
a.fill(value); //将value赋值给a的每一元素
a1.swap(a2); // a1和a2元素互换(必须a1和a2长度一样)
#include
using namespace std;
#include
int value = 10;
array<int, 5> arr{0};
vector<int> v1;// 声明并且定义
vector<int> v2(arr.begin(),arr.begin() + n); // 通过迭代器或指针初始化
vector<int> v3(value) // 有value个0
vector<int> v4(value, 1) // 有value个1
vector<int> v5{ 1, 2, 3, 4, 5}; //声明定义后直接初始化
//接上文
//1. 迭代器,略,同array
//2. 容器元素数量相关
v1.size();// 同array,注意max_size()也有,表示能够容纳最大元素个数,两者不同
v1.empty(); //同array
v1.resize(value); //改变实际元素的个数
v1.reserve(value); //扩大容量到value大小
v1.capacity() //返回当前容量
v1.shrink_to_fit() // 将容量减少到等于当前元素实际所使用的大小
//3.元素访问和赋值
v1[0];v1.at();v1.front();v1.back();v1.data(); //同array
v1.assign(10, value)//分配常量值的语法,会覆盖之前的部分 10个value
v1.assign(v2.begin(),v2.begin() + 2) //从数组分配,此处可以换为指针
v1.assign({ 1, 2, 3, 3, 2, 1});//分配为一个列表
//4.增加和删除元素
v1.push_back(value);v1.emplace_back(value);// 在尾端加入容器,后者可以直接调用对应构造函数。
//push_back:容器尾端添加临时对象,然后调用构造函数,最后使用移动构造函数(注意不是拷贝构造)
//emplace_back:尾端直接添加一个元素,调用构造函数原地构造,不触发拷贝和移动构造。(如果传入已构造好的元素,那么与push_back一致)
v1.emplace(v1.end(), value); // 在指定的位置直接生成一个元素
//以下均返回第一个新插入元素位置的迭代器
v1.insert(v1.begin(),value); //插入结果:{value, v1[0],...}
v1.insert(v1.begin(),3,value); //在该位置插入3个value
v1.insert(v1.begin(),v1.begin() + 2,v1.begin() + 3); //在begin位置插入v1[2]
v1.insert(v1.begin(),{value}); // 该位置插入{}中元素
//删除元素后面一个迭代器
v1.erase(v1.begin()); //删除第一个元素
v1.erase(v1.begin(), v1.begin() + 2); // 删除第一个,第二个元素
v1.pop_back();//最后一个元素
v1.clear(); // 清楚所有元素
//5.其他
v1.swap(v2); //v1, v2互换
考虑到数组出现频率高(如:试题数据给出形式,解答中构造辅助数组,答案返回值等)。并且是其他算法和技巧实现与结合的基础,比如排序算法,双指针,动态规划等。所以本部分试题总结仅考虑一下内容:
针对数组静态查找,如果没有任何技巧,往往时间复杂度为O(n);
如果针对数据特点,能够制定某种测略,快速排除不符合要求(不必要的)答案,能够快速进行查找,从而使得复杂度有望降低找O(logn)水平:例如二分查找。下面是针对二分查找的补充说明:
二分查找最直接的应用是能够对有序数组进行查找。但注意不一定数组为有序,只需要满足使用某种测略,排除不符合要求(不必要的)答案即可;
有些时候,可以通过预处理等技巧构造符合1中条件的数组,从而降低复杂度;并且该技巧可以不构造数组,针对数据存在区间直接进行二分法操作。(由于博客主题,这里不例举题目)
下面是一些扩展性的优化讨论(P.S: 先放在这里,后面进行修改):二分法的核心是根据数据和场景状况,快速排除错误答案。我们根据这一点,将一些常用的限制条件放宽,进一步做出如下简单讨论:
1). 由于有些场景受限,对于一些题目,最优解法不一定需要等分,而是需要根据题目条件灵活变化。如:扔鸡蛋问题。
a. 这里先简单讨论经典情况:2个鸡蛋100层楼。由于可尝试次数与鸡蛋状况(碎与没碎)绑定的条件限制。将两个鸡蛋作用分为粗调和细调两种,接下来就是进一步讨论粗调和细调选取范围和方案。
b. 其它情况讨论中可以发现动态规划能够优化这种策略:思路为k-1个鸡蛋用来逐步确定范围,最后一个鸡蛋。由于博客主题,本部分讨论省略。
2). 二分法中的“二”基于的是一维数据”属于“和”不属于“对结果的可能性进行等可能划分下的优化解答。如果场景不止需要对两种情况进行优化,我们可以根据结果的可能性重新划分,快速排除可能性存在的空间。
如:称量硬币。题意大致为给出一些硬币,硬币中有一枚可能是假币。如果是假币,可能轻或者重,要求尽可能少的使用不带砝码的天平找出那枚硬币(可能没有)。如果有,请说明它比其他硬币重还是轻。这里给出一个参考解答过程。
搜索长度未知的有序数组
思路:
a. 需要找到left和right边界。不妨设left = 0,right = 1。根据right元素与target关系调整left和right边界。
b. [left,right]上使用二分查找。
核心代码
/**
* // This is the ArrayReader's API interface.
* // You should not implement it, or speculate about its implementation
* class ArrayReader {
* public:
* int get(int index);
* };
*/
class Solution {
public:
int search(const ArrayReader& reader, int target) {
// 补丁:如果第一个值 left = 0 位置就是 target,防止后续查找丢失情况。
int left = 0, right = 1;
if (reader.get(left) == target) {
return left;
}
// 确定左右边界
while (reader.get(right) < target) {
left = right;
right <<= 1; // right扩大一倍搜索
}
// 二分查找
while (left < right) {
int mid = left + ((right - left) >> 1);
if (reader.get(mid) < target) {
left = mid + 1;
} else if (reader.get(mid) >= target) {
right = mid;
}
}
return reader.get(right) == target ? right: -1;
}
};
时间复杂度:O(logn)
空间复杂度: O(1)
搜索旋转排序数组
思路:
a. 由于本题局部有序,且可以分为两段。我们尝试改进原版的二分查找求解。
b. 将原数组分为两个part,我们改进判断条件。先使用left位置和mid看mid位于左边段还是右边段。(数字大于nums[left]段(左段)和数字小于nums[left]段(右段))
c. 能够如此得原因是mid要么命中0->index段,要么命中index->right段。(index为原数组随机旋转下标)并且mid可以将这两段的每一段再分为两个小part。
d. 在上面命中的那一块分为两个part内分别查找,迭代查找区间。
核心代码
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
return mid;
// mid命中哪一个part
} else if (nums[left] <= nums[mid]) {
// 分别查找每一段子part,没有排除一部分区间
if(target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
};
时间复杂度:O(logn)
空间复杂度: O(1)
山脉数组的峰顶索引
思路:
a. 二分法,根据mid与mid + 1, mid - 1大小更新参数即可。
b. 代码优化:只需要mid与mid+1比较大小。此时需要用ans记住峰值位置。
核心代码
// 原始思路进行的代码
int peakIndexInMountainArray(vector<int>& nums) {
int left = 1, right = nums.size() - 2;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] > nums[mid - 1] && nums[mid] > nums[mid + 1]) {
return mid;
} else if (nums[mid] > nums[mid - 1] && nums[mid] < nums[mid + 1]) {
left = mid + 1;
} else {
right = mid - 1;
}
return -1;
}
}
// 下面是根据最开始的思路进行一个小优化
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
int left = 1, right = arr.size() - 2;
int ans = 0;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (arr[mid] > arr[mid + 1]) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
};
时间复杂度:O(logn)
空间复杂度: O(1)
扩展:局部最小值 :在一个无序数组(任何两个相邻数不等)中找出一个局部最小值。(视频1h42min处)
其他注意:
这里主要是二元数组,举出一个例子。
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
//排除异常情况
if (matrix.size() == 0) {
return false;
}
// 右上角开始搜索
int pos_x = 0, pos_y = matrix[0].size() - 1;
while (pos_x >= 0 && pos_x < matrix.size() && pos_y >= 0 && pos_y < matrix[0].size()) {
if (matrix[pos_x][pos_y] > target) {
--pos_y;
} else if (matrix[pos_x][pos_y] < target) {
++pos_x;
} else {
return true;
}
}
return false;
}
};
时间复杂度:O(m+n)
空间复杂度:O(1)
这里往往对code有一定要求:控制流程一定要对,不然很容易错,还且有一些边界情况。
数组原地修改有很多经典题目,主要技巧是双指针(例如快排partition过程)。
对于多维数组,可以类似一维数组考虑,关键是调度转移过程。
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
// left 指向处理过的最后一个奇数区间右边界(不含)
// right 指向处理过的第一个偶数区间左边界(不含)
int left = 0, right = nums.size() - 1;
while (left <= right) {
if (nums[left] % 2 == 0) {
swap(nums[right--], nums[left]);
} else {
++left;
}
/* 这里不需要使得left 和 right 同时移动,即添加如下代码
* 可能导致 left 或 right 越界 (例如:nums全部为奇数)
* 如果要添加需要添加判断越界条件
if (nums[right] % 2 == 1) {
swap(nums[left++], nums[right]);
} else {
--right;
}
*/
}
return nums;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
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 + 1)/ 2; ++j) {
// 注意奇数时候,调度col 为(n + 1)/ 2; 偶数依旧为 n/2(C++能够向下取整)
int temp = matrix[i][j];
matrix[i][j] = matrix[row - 1 - j][i];
matrix[row - 1 - j][i] = matrix[row - 1 - i][row - 1 - j];
matrix[row - 1 - i][row - 1 - j] = matrix[j][row - 1 - i];
matrix[j][row - 1 - i] = temp;
}
}
}
};
时间复杂度:O( n 2 n^{2} n2)
空间复杂度:O(1)
这里主要是多维数组的遍历,同样也是弄清楚调度转移过程,完成code即可。
class Solution {
public:
string convert(string s, int numRows) {
if (numRows == 1 || numRows >= s.size()) {
return s;
}
string ans(s.size(), ' ');
int pos = 0;
for (int row = 0; row < numRows; ++row) { // 每一行例举
for (int k = 0; k * (numRows - 1) * 2 + row < s.size(); ++k) { // 对每一个行的小块例举
ans[pos++] = s[k * (numRows - 1) * 2 + row];
if (row > 0 && row < numRows - 1 && (k + 1) * (numRows - 1) * 2 - row < s.size()) { // 第1~ numRows - 2 行需要每次额外添加新的元素,注意元素要有
ans[pos++] = s[(k + 1) * (numRows - 1) * 2 - row];
}
}
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int rows = matrix.size(), cols = matrix[0].size();
vector<int> arr(rows * cols, 0);
int left = 0, right = cols - 1, bottom = rows - 1, top = 0; // 四个边界,类似剥洋葱式遍历。
int pos = 0; // 添加值的下标
while (pos < rows * cols) { // 剥去其中一层
for (int col = left; col <= right; ++col) {
arr[pos++] = matrix[top][col];
}
for (int row = top + 1; row <= bottom; ++row) {
arr[pos++] = matrix[row][right];
}
if (left < right && top < bottom) { //另外一半的遍历是否要继续
for (int col = right - 1; col >= left; --col) {
arr[pos++] = matrix[bottom][col];
}
for (int row = bottom - 1; row > top; --row) {
arr[pos++] = matrix[row][left];
}
}
++left;
--right;
++top;
--bottom;
}
return arr;
}
};
时间复杂度:O(nm)
空间复杂度:O(1)
数据流场景是现实中面临十分经典的场景。这个场景数据是以流的形式出现,并且随时需要快速返回需要信息。算法优化往往根据场景单独进行,属于比较难的问题。以下举出几个案例:
class MedianFinder {
priority_queue<int, vector<int>, less<int>> quemin; // 大根堆,存储后面的数字
priority_queue<int, vector<int>, greater<int>> quemax; // 小根堆,存储前面的数字
public:
MedianFinder() {
}
void addNum(int num) {
if (quemin.empty() || num <= quemin.top()) {
quemin.push(num);
if (quemin.size() > quemax.size() + 1) {
quemax.push(quemin.top());
quemin.pop();
}
} else {
quemax.push(num);
if (quemax.size() > quemin.size()) {
quemin.push(quemax.top());
quemax.pop();
}
}
}
double findMedian() {
if (quemin.size() > quemax.size()) {
return quemin.top();
}
return (quemax.top() + quemin.top()) / 2.0;
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
时间复杂度:addNum: O(logn), findMedian:O(1)
空间复杂度:O(n)
class KthLargest {
priority_queue<int, vector<int>, greater<int>> q;
int k;
public:
KthLargest(int k, vector<int>& nums) {
this->k = k;
for (auto& x: nums) {
add(x);
}
}
int add(int val) {
q.push(val);
if (q.size() > k) {
q.pop();
}
return q.top();
}
};
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest* obj = new KthLargest(k, nums);
* int param_1 = obj->add(val);
*/
时间复杂度:add: O(logk), 初始化 O(nlogk)
空间复杂度:O(k)
其他补充:
滑动窗口技巧是指在一个特定窗口中处理某一任务,一般试题偏难。(具体技巧在字符串中总结)
这里贴出一个视频和帖子。总结了相关技巧和使用条件模板。下面直接给出案例:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q; // 存下标原因是数组索引方便,并且后面还需排除不在窗口内下标。
// 初始化单调队列
for (int i = 0; i < k; ++i) {
while(!q.empty() && nums[i] >= nums[q.back()]) { // 与队尾元素比较,队尾元素小的话直接抛弃
q.pop_back();
}
q.push_back(i);
}
vector<int> ans{nums[q.front()]} ;
for (int i = k; i < nums.size(); ++i) {
// 不停重复上操作
while(!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
// 添加一个操作:防止队列元素过时(相比窗口左端)
while (q.front() <= i - k) {
q.pop_front();
}
ans.push_back(nums[q.front()]);
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(k)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> st;
int right = 0, ans = 0;
for (int left = 0; left < s.size(); ++left) {
// 如果非初次进入该处,表明统计区间有重复字符
// 此时left 已经移动到下一个位置,注意删除起点为i - 1
if(left != 0) {
st.erase(s[left - 1]);
}
//统计非重复字符
while(right < s.size() && !st.count(s[right])) {
// 不断移动右指针
st.insert(s[right]);
++right;
}
ans = max(ans, right - left);
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(k) // k个字符在字符串中出现
这类问题十分经典。方法主要是贪心和动态规划。注意子序列可以不连续,子数组必须连续。这里举出几个例子:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans = nums[0]; // 防止全部为负数
int sum = 0;
for (int i = 0; i < nums.size(); ++i) {
sum = max(nums[i], sum + nums[i]); // 如果遍历的局部和为负数,直接除去负数部分遍历和,保留当前元素。
ans = max(ans, sum); //与前面所存留的局部和取最大。
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size(), len2 = nums2.size();
int ans = 0;
// 移动nums1,进行一次对齐匹配
for (int i = 0; i < len1; ++i) {
int len = min(len2, len1 - i); // 计算对齐后共有长度
ans = max(ans, maxLength(nums1, nums2, i, 0, len));
}
// 移动nums2,进行一次对齐匹配
for (int i = 0; i < len2; ++i) {
int len = min(len1, len2 - i); // 计算对齐后共有长度
ans = max(ans, maxLength(nums1, nums2, 0, i, len));
}
return ans;
}
// off1, off2移动对齐后,一一匹配能够找到最长子数组长度
int maxLength(vector<int>& nums1, vector<int>& nums2, int off1, int off2, int len) {
int ans = 0 , part_len = 0;
for (int i = 0; i < len; ++i) {
if (nums1[off1 + i] == nums2[off2 + i]) {
++part_len;//统计所有相同的子数组长度
} else { // 一个断掉直接置零
part_len = 0;
}
ans = max(ans, part_len);
}
return ans;
}
};
时间复杂度:O(O((N+M)×min(N,M)))
空间复杂度:O(1)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(), 0);
for (int i = 0; i < nums.size(); ++i) {
dp[i] = 1; // 至少最长序列为1,即只有自己
//向前遍历,更新当前dp
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) { // 满足增加1的条件
dp[i] = max(dp[i], dp[j] + 1); // 按照转移方程更新
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
时间复杂度:O( n 2 n^{2} n2)
空间复杂度:O(n)
其他补充:
这类题目还有很多,并且解题方法类型也多种多样。比如:
递增子序列
和为 K 的子数组
最长重复子串
这些试题场景比较特殊,一般用特别的算法优化这些问题(例如贪心)。下面直接举出一些案例:
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
vector<int> ans;
int elem1 = 0, elem2 = 0; // 候选元素
int vote1 = 0, vote2 = 0; // 血量(遇到与候选匹配加1,否则减去)
for (int x : nums) {
// 元素计数
if (vote1 > 0 && x == elem1) {
++vote1;
} else if (vote2 > 0 && x == elem2) {
++vote2;
// 第一次选择元素或候选元素统计时被抵消了
} else if (vote1 == 0) {
elem1 = x;
++vote1;
} else if (vote2 == 0) {
elem2 = x;
++vote2;
} else {
--vote1;
--vote2;
}
}
// 得到候选解后需要统计是否两个都是满足题意的(统计)
int cnt1 = 0, cnt2 = 0;
for (auto x : nums) {
if (vote1 > 0 && x == elem1) {
++cnt1;
}
if (vote2 > 0 && x == elem2) {
++cnt2;
}
}
if (vote1 > 0 && cnt1 > nums.size() / 3) {
ans.push_back(elem1);
}
if (vote2 > 0 && cnt2 > nums.size() / 3) {
ans.push_back(elem2);
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:
int jump(vector<int>& nums) {
int maxpos = 0; // 目前能够到达的最远位置
int step = 0; // 跳跃次数
int end = 0; // 上次跳跃可以达到的右边界
for (int i = 0; i < nums.size() - 1; ++i) {
maxpos = max(maxpos, i + nums[i]); // 更新目前能够跳跃的最远位置
if (i == end) { // 如果当前已经跳跃到最远位置
end = maxpos; // 更新信息
++step; //一定能够从上一次的位置跳一步到达end
}
}
return step;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int ans = 0;
while (left < right) {
int area = min(height[left], height[right]) * (right - left);
ans = max(area , ans);
if (height[left] <= height[right]) { // 哪边较小往,移动哪边指针。
++left;
} else {
--right;
}
}
return ans;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:
string largestNumber(vector<int>& nums) {
// 排序比较函数如下:x(y) > y(x)
auto f = [] (const int x,const int y) {
long sx = 10, sy = 10;
while (sx <= x) {
sx *= 10;
}
while (sy <= y) {
sy *= 10;
}
return sy * x + y > sx * y + x;
};
sort(nums.begin(), nums.end(), f);
// 防止先导0出现(此时数组全为0)
if (nums[0] == 0) {
return "0";
}
string ans;
for (int x : nums) {
ans += to_string(x);
}
return ans;
}
};
时间复杂度:O(nlogn*log(INT_MAX))
空间复杂度:O(logn)