算法入门 - 数组
1. 二维数组中的查找
查找的本质就是排除,一次排除的越多,查找的越快。
右上角开始遍历
class Solution {
public:
//查找的本质就是排除,一次排除的越多,查找的越快
//由于数组左向右递增,上向下递增,故从右上角开始判断:
// 当当前元素比右上元素大,说明元素不会出现在当前行,排除当前行
// 当当前元素比右上元素小,说明元素不会出现在当前列,排除当前列
//排除当前行就i++,排除当前列就j--
bool Find(int target, vector<vector<int> > array) {
//写出右上元素下标
int i = 0;
int j = array[i].size() - 1;
while (i < array.size() && j >= 0) {
//当前元素比右上元素小
if (target < array[i][j]) {
j--;
}
//当前元素比右上元素大
else if (target > array[i][j]) {
i++;
}
else {
return true;
}
}
return false;
}
};
左上角开始遍历
class Solution {
public:
//由于数组是从左向右递增的,采用从左向右找的策略,
//从第1行开始,判断元素是否小于a[0][1],如果小说明不存在,如果大,在和a[0][n-1]比较
// 如果小说明可能在第一行的中间,遂遍历第一行,有则有没有就没有。
// 如果大,说明可能在下面,重复上述判断步骤。
//从第i行开始,判断元素是否小于a[i][1],如果小说明不存在,如果大,在和a[i][n-1]比较
// 如果小说明可能在第i行的中间,遂遍历第i行,有则有没有就没有。
//直至遍历结束。
bool Find(int target, vector<vector<int> > array) {
//空数组特判
for (int i = 0; i < array.size(); i++) {
if (target < array[i][0]) {
//元素小于a[i][1] -- 当前行的起始元素
return false;
}
else if (target < array[i][array[i].size() - 1]) {
//元素小于a[0][n-1] -- 当前行的末尾元素
//遍历当前行,寻找匹配元素
for (int j = 1; j < array[i].size() - 1; j++) {
if (target == array[i][j]) {
return true;
}
}
}
else if (target == array[i][0]) {
return true;
}
else if (target == array[i][array[i].size() - 1]) {
return true;
}
}
return false;
}
};
2. 旋转数组的最小值
非递减数组旋转后,会将数组分为前后两部分,两部分都是递增序列。
可以看出非递减数组任意左旋后(除全旋)都满足最左边的元素大于等于最右边的元素。
二分查找则是将数组中间的值和最左最右的元素比较,以判断最小值在中间元素的左边还是右边或是中间。
- 若 mid 元素小于 left 元素,说明左旋位置在左区间中,遂收紧区间 right = mid
- 若 mid 元素大于 left 元素,说明左旋位置在右区间中,遂收紧区间 left = mid
- 若 mid 元素等于 left 元素等于 right 元素,则无法判断左旋位置在左右区间,只能线性遍历。
二分法
class Solution {
public:
//二分法:从中间开始判断,不断收紧,寻找最小值(将最小值所在位置定义为左旋位置)
//定义指针:left mid right
// 若mid元素小于left元素,说明[left.mid]区间不满足非递减条件,
// 故左旋位置在左区间中,遂收紧区间right = mid
// 若mid元素大于left元素,说明[left.mid]区间满足非递减条件,
// 故左旋位置在右区间中,遂收紧区间left = mid
// 若mid元素==left元素==right元素,则无法判断直接线性查找即可。
int minNumberInRotateArray(vector<int> nums) {
int left = 0, right = nums.size() - 1;
int mid = 0;
//除全等和全旋的特殊情况外,所有左旋结果都满足nums[left]>=nums[right]
//遇全等和全旋的特殊情况,不进循环,直接返回首元素即可。
//使用该表达式作为判断条件,可以保证二分查找的区间是满足条件的有效区间
while (nums[left] >= nums[right]) {
//元素个数为2是返回right即可,因为只要旋转右面的就是小的
if (right - left == 1) {
mid = right;
break;
}
mid = (left + right) / 2;
//无法判断,线性查找
if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
int ret = nums[left];
for (int i = left + 1; i < right ; i++) {
if (ret > nums[i]) {
ret = nums[i];
}
}
return ret;
}
//二分查找
if (nums[mid] >= nums[left]) {
left = mid;
}
else {
right = mid;
}
}
return nums[mid];
}
};
遍历法
线性遍历的方法效率都差不多,查找看的是排除的效率,二分法才是排除的高效之选。
class Solution {
public:
//由于数组是个非递减数组的左旋结果,遂遍历数组,
// 当当前元素比下一个元素大时,说明该位置就是左旋的位置,当前的下一个元素就是最小值
//特殊情况:当遍历数组没有发现上述情况,说明数组元素完全相等或全部左旋,统一返回首元素即可。
int minNumberInRotateArray(vector<int> rotateArray) {
for (int i = 0; i < rotateArray.size() - 1; i++) {
if (rotateArray[i] > rotateArray[i + 1]) {
return rotateArray[i + 1];
}
}
//特殊情况
return rotateArray[0];
}
};
3. 调整数组奇偶顺序
调整奇数至偶数之前(相对顺序不变),本质就是向后寻找奇数与其之前的偶数序列交换。
相对顺序不变
找奇数以确定偶数区间是最优解。
class Solution {
public:
//遍历数组,跳过偶数,直到遇到奇数时,将前面的偶数序列,后移一位并将奇数放到前面。
void reOrderArray(vector<int>& nums) {
int begin = 0;
//遍历数组
for (int end = 0; end < nums.size(); end++) {
//遇到奇数时:[begin, end)就是偶数区间,begin所在位置就是起始位置
if ((nums[end] & 1) == 1) {
int tmp = nums[end]; //提前保存奇数
//后移偶数序列
for (int j = end; j > begin; j--) {
nums[j] = nums[j - 1];
}
nums[begin] = tmp; //前插奇数
begin++;
}
}
}
};
线性遍历
class Solution {
public:
//开辟等大小的数组,遍历两边一遍找奇一遍找偶,再向新数组中拷贝。这样可以保证相对位置不变
void reOrderArray(vector<int>& nums) {
int begin = 0, end = nums.size() - 1;
vector<int> tmp(nums);//定义暂存数组
int index = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] % 2 == 1) {
tmp[index++] = nums[i];
}
}
for (int i = 0; i < nums.size(); i++) {
if (nums[i] % 2 == 0) {
tmp[index++] = nums[i];
}
}
swap(tmp, nums);
}
};
遍历两边一遍找奇一遍找偶,和定义头尾指针头找奇尾找偶,两种方法效率上没区别。
相对顺序改变
class Solution {
public:
//采用头尾指针的方式;头指针找偶数,尾指针找奇数,两指针分别向中间遍历,并交换,直至相遇。
//这样能使奇数在前偶数在后。
void reOrderArray(vector<int>& nums) {
int begin = 0, end = nums.size() - 1;
while (begin < end) {
while (begin < end && nums[begin] % 2 != 0) {
//头指针找偶数
begin++;
}
while (begin < end && nums[end] % 2 != 1) {
//尾指针找奇数
end--;
}
//交换
swap(nums[begin], nums[end]);
begin++, end--;
}
}
};
4. 出现次数过半数字
抵消不同元素
遇到相异的元素,可以将其抵消掉,最后剩下的元素就是出现次数最多的数字。
class Solution {
public:
//抵消不同元素,最后剩下的元素就是出现超出一半的数字
//定义当前元素值和出现次数变量,向后遍历数组:
// 元素变量和当前元素相同,次数++,用来统计次数
// 元素变量和当前元素相异。次数--,用来模拟抵消元素
// 当次数变为0时,相当于当前元素抵消完了,更新元素变量并重置次数,继续向后遍历
int MoreThanHalfNum_Solution(vector<int> nums) {
int num = nums[0]; //从首元素开始,提出出来以便之后的操作
int times = 1;
for (int i = 1; i < nums.size(); i++) {
if (num == nums[i]) {
//遇到相同元素
times++;
}
else {
//遇到相异元素
times--;
}
if (times == 0) {
//抵消完毕
num = nums[i]; //将当前元素设置为待比较元素(前面的都已经抵消完)
times = 1; //修正次数
}
}
return num;
}
};
统计出现次数
用图存储各个元素和次数的对应关系。
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int> nums) {
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
auto it = map.find(nums[i]);
if (it == map.end()) {
//图中不存在该元素
map.insert({
nums[i], 1});//插入元素和次数
}
else {
//图中存在
map[nums[i]]++; //次数++
}
if (map[nums[i]] > nums.size() / 2) {
//出现次数超出一般
return nums[i];
}
}
return 0;
}
};
利用长度为2的数组存储数值和次数。
class Solution {
public:
//利用数组存储元素和其出现个数,遍历寻找最大值
int MoreThanHalfNum_Solution(vector<int> numbers) {
sort(numbers.begin(), numbers.end());
int begin = 0, end = 0;
int retArr[2] = {
0 };
//前后遍历寻找当前元素的最后一个位置
while (end < numbers.size()) {
while (end < numbers.size()) {
if (numbers[end] != numbers[begin]) {
break;
}
end++;//向后遍历
}
if ((end - begin) > retArr[0]) {
retArr[0] = end - begin;//存入数组
retArr[1] = numbers[begin];
}
//把begin变成下一个元素的起始位置,继续向后遍历
begin = end;
}
return retArr[1];
}
};
排序后判断对应位置
排序后,老老实实地进行线性查找。
class Solution {
public:
//先排序,从头开始遍历,
// 若当前元素和超其数组长度一半的位置的元素相等,则该数字即为出现次数过半数字
int MoreThanHalfNum_Solution(vector<int> nums) {
sort(nums.begin(), nums.end());
int n = nums.size();
for (size_t i = 0; i < n / 2 + 1; i++) {
if (nums[i] == nums[i + n / 2]) {
return nums[i];
}
}
return -1;
}
};
排序后检查中间元素是否出现次数超出数组长度一半。
class Solution {
public:
//先排序,个数超出数组长度一半的元素必然在数组中部,判断该元素出现的个数:
//从中部位置开始向前后双指针遍历,相同则++不同则放弃。检查元素出现个数是否超出一半。
int MoreThanHalfNum_Solution(vector<int> nums) {
sort(nums.begin(), nums.end());
int half = nums.size() / 2;
int cnt = 1;//half位置本值占一个
int i = 1;
while (i <= half) {
if (nums[half - i] == nums[half]) {
//half位置和之前元素比较
cnt++;
}
if (nums[half] == nums[half + i]) {
//half位置和之后元素比较
cnt++;
}
i++;
}
if (cnt > half) {
// 元素个数超出数组长度的一半
return nums[half];
}
return 0;
}
};
“投机取巧”的方式:
class Solution {
public:
//先排序,个数超出数组长度一半的元素必然在数组中部,直接返回中间位置的值即可。
int MoreThanHalfNum_Solution(vector<int> nums) {
sort(nums.begin(), nums.end());
return nums[nums.size() / 2];
}
};