千里之行,始于足下;代码还是得自己写,才能记忆深刻,这是一个需要累积时间的过程,不是说你在图书馆待上一周,看着书就会了的事情。也由此,在此总结数组合矩阵相关的问题,自己写代码实现(c++)
目录
3. 正数数组中累计和为k的 最长子数组长度 (双指针弄开头)
4. 累计和为k的 最长子数组长度 (k==arr[j+1,m]->sum[m]-sum[j])
5. 累计和<=k的 最长子数组长度 (helpArr存储curMax,转换为>=sum-k的最短路径)
6. 二分法find大于k的第一个数||find大于k的最后一个数 (上一题的基础)
7. 奇数在奇数索引上,偶数在偶数索引上 (双指针 odd=1;even=0;和arr[end]做就交换)
8. 奇数在前,偶数在后 (双指针,前后各一个,各控制奇数和偶数)
9. 最长可整合子数组长度 (假设i-j满足,for (int i=0...for (int j=i))
10. 子数组的最大累乘积 归纳法-动态规划 (分析每个位置 i 的可能性,主要是从i-1已知了某些条件的角度出发)
14. 查找 最小的K个数【Partition or 大根堆小根堆】
17. 查找 超过一半(N/2)的数 (构建一个map,记录res和count)
18. 查找 超过N/K次的数 (构建一个map[num,count])k-1
19. 需要排序的最短子数组长度 (从右向左遍历【左边界】+从左向右遍历【右边界】)
1. 快速排序
主要思想:
- 构造partition函数(返回一个pivot,左边都是比pivot小的,右边都是比pivot大的)
- partition主要是弄一个small,记录当前小于pivot的index在哪里(small-start就是有几个小的),如果小于pivot就和当前small位置交换(也就是不断将小的数,放到前面)然后small++
- qsort 调用partition,然后不断递归qsort排pivot左边和右边。
==================================================================
需要注意:
- 注意qsort参数(int arr[],int length,int start,int end)basecase记得加上
- 随机函数的 srand((unsigned)time(NULL)); rand%(end-start)+start
// Qsort.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
//#include <algorithm>
#include <time.h>
using namespace std;
int getRand(int start, int end) {
srand((unsigned)time(NULL));//老是记不住
return rand() % (end - start) + start;//注意是取余数
}
int partition_my(int arr[], int length, int start, int end) {
if (arr == nullptr || length <=0 || start < 0 || end >= length) throw new exception("invalid para");//没有return
int pivot = getRand(start, end);
int small = start;
std::swap(arr[pivot], arr[end]);
for (int i = start; i < end; i++) {
if (arr[i] < arr[end]) {
if(small!=i){
std::swap(arr[i],arr[small]);
}
small++;//交换一次,证明small确定一个,
}
}
std::swap(arr[small], arr[end]);//最后small左边都是小于arr【end】的数
return small;
}
void qsort_my(int arr[], int length, int start, int end) {
if (start == end) { return; }//注意basecase
int pivot = partition_my(arr, length, start, end);
if (pivot > start) {
qsort_my(arr, length, start, pivot - 1);
}
if (pivot < end) {
qsort_my(arr, length, pivot + 1,end);
}
}
void qsort_m(int arr[], int length)
{
qsort_my(arr, length, 0, length - 1);
}
int main()
{
int arr[5] = { 5,3,1,2,4 };
qsort_m(arr, 5);
return 0;
}
2. 归并排序
注意点:
- 归并排序,记住是先用黑盒,再解的黑盒。空间复杂度是O(N)
- MergeSort(arr,L,mid,temp);//注意这里是mid,不是mid-1,和qsort不一样
- while (i<=mid && j<=right){ //while里两个,没用for,不满足一个就退出,注意是=
/* Merge sort in C++ */
#include <cstdio>
#include <iostream>
using namespace std;
void MergeSort(int *arr,length){
int *temp = new int[length];// int temp[length]
MergeSort(arr,0,length-1,temp);
delete[] temp ;
}
void MergeSort(int *arr,int L,int R,int *temp){
if(L==R) return;
mid=L+(R-L)>>2;
if(L<R){
MergeSort(arr,L,mid,temp);//注意这里是mid,不是mid-1,和qsort不一样
MergeSort(arr,mid+1,R,temp);
merge(arr,L,mid,R,temp);
}
}
void merge(int *arr,int left,int mid,int right,int *temp)
{
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){//while里两个,不满足一个就退出,注意是=
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
3. 正数数组中累计和为k的 最长子数组长度 (双指针弄开头)
主要思想:
- 双指针L,R,弄到最左边,然后看情况移动L和R,更新maxlen,和sum
int getMax(int arr[],int length,int k) {
if (arr == nullptr || length <= 0 || k <= 0)return 0;
int l = 0;int r = 0;
int len = 0;int sum = 0;
for (int i = 0; i < length - 1; i++) {
if (sum == k) {
len = max(len, r - l + 1);
sum -= arr[l];
l++;
}
if (sum < k) {
if(r<length-1){
r++;
sum += arr[r];
}
}
if (sum > k) {
sum -= arr[l];
l++;
}
}
return len;//如果返回是0说明不存在
}
int main()
{
int arr[5] = { 1,2,1,1,1 };
int a = getMax(arr, 5,3);
return 0;
}
4. 累计和为k的 最长子数组长度 (k==arr[j+1,m]->sum[m]-sum[j])
主要思想:【可直接看代码注释】
- 首先假设最终的所求数组是以m为结尾的(这样就是一个从左向右的遍历的思维)arr[j+1,m]那么sum怎么球呢?
- k?=arr[j+1,m]=sum[m]-sum[j],sum从0开始,m是遍历用的,求最长的j+1,m,其实就是求满足sum-k时最短的j,如下图
==================================================================
- 讨论的是以i为结尾的满足条件的最长子串(for (int i = 0; i < length; i++) )
- 如果 sum-k 存在,说明以i结尾有累积和 k
- 另外一个如果sum不存在在map中,加入map[sum]=i;
==================================================================
注意点:
- 因为数组是从0开始index的,比如0-2是最长,那么返回长度3,所以首先需要将map.put(0,-1)进去
int getMax(int arr[], int length ,int k) {
if (arr == nullptr || length < 0)return 0;
map<int,int> temp;
temp[0] = -1;
int sum = 0;
int len = 0;
for (int i = 0; i < length; i++) {
//sum-k是否存在
sum += arr[i];
map<int,int>::iterator find = temp.find(sum - k);
if (find != temp.end()) {//如果sum - k存在!
len=max(len,i- (find->second));//注意!!->second没有();!
}
//注意这里不能写else
if(temp.find(sum)==temp.end())//如果sum不存在!!
{
temp[sum] = i;//别写反了
}
}
return len;
}
int main() {
int arr[6] = { 1,2,1,1,-1,2 };
int a = getMax(arr, 5, 3);
return 0;
}
5. 累计和<=k的 最长子数组长度 (helpArr存储curMax,转换为>=sum-k的最短路径)
主要思想:
- 求出curSum
- 求<=k的 最长子数组长度那就是求>=curSum-k的最短路径,所以需要一个辅助数组helpArr存储目前为止的最大值
- 然后在helpArr中找>=curSum-k的第一个(二分查找即可)
==================================================================
注意点:
helpArr最开头都要加0,表示什么也不加的情况。因为helpArr多一个length长度,不用-1.
int getLessIndex(int* arr, int i, int k)
{// 求数组arr[0...i]内》k的第一个数字 a
// 注意返回值,就是arr中的第几个 第一次满足了》=k,
// 比如返回0,表示什么都不加,返回1,表示第一个也是就是arr[0],满足这个条件
if (arr == nullptr || length < 0 )//注意length是有可能=1的
return -1;
int res = - 1, left = 0, right = i;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] >= k)
{
res = mid;//值得注意的是不断二分,最终卡住的就是第一个
right = mid - 1;
}
else
left = mid + 1;
}
return res;
}
int getMax(int arr[], int length ,int k) {
if (arr == nullptr || length < 0)return 0;
int res = 0;
int *helparr = new int[length+1];
helparr[0] = 0;//存储此前最大值
int sum=0;//存储0-i的和
for (int i = 0; i <= length; i++) {
sum += arr[i];
helparr[i + 1] = max(sum, helparr[i]);
}
sum = 0;
int len = 0;
int j = -1;
//注意:遍历原数组arr的长度,求的是arr每个位置》sum-k的第一个数字
for (int i = 0; i < length; i++) {
sum += arr[i];//当前的sum
j = getLessIndex(helparr,i,sum-k);//求数组helparr[0...i]内》sum - k的第一个数字
if (j != -1)res = max(res, i - j + 1);
}
return res;
}
int main() {
int arr[5] = { 1,2,-1,5,-2};
int a = getMax(arr, 5, 3);
return 0;
}
int getMax(int arr[], int length ,int k) {
if (arr == nullptr || length < 0)return 0;
int res = 0;
int *helparr = new int[length+1];
helparr[0] = 0;//存储此前最大值
int sum=0;//存储0-i的和
for (int i = 0; i <= length; i++) {
sum += arr[i];
helparr[i + 1] = max(sum, helparr[i]);
}
sum = 0;
int len = 0;
int j = 0;
for (int i = 0; i <= length; i++) {
sum += arr[i];//当前的sum
j = getLessIndex(helparr, i, sum - k);
if (j != -1)res = max(res, i - j + 1);
}
delete[]helparr;
return res;
}
int main() {
int arr[5] = { 1,2,-1,5,-2};
int a = getMax(arr, 5, 3);
return 0;
}
6. 二分法find大于k的第一个数||find大于k的最后一个数 (上一题的基础)
while (left <= right)//这里的等于号很重要,只有这里等于才能找到第一个,凡是有可能是最后只剩 一个数的情况,都是需要加=
比如0123你找大于=3的第一个数,只有有=才能包括某一个数
int getLessIndex(int* arr, int length, int k)
{// 二分法find大于k的第一个数
if (arr == nullptr || length <= 0 || arr[length - 1] <= k)
return -1;
int res = length - 1,left=0,right=length-1;
while (left <= right)//这里的等于号很重要,只有这里等于才能找到第一个
{
int mid = left + (right - left) / 2;
if (arr[mid] > k)
{
res = mid;//值得注意的是不断二分,最终卡住的就是第一个
right = mid - 1;
}
else
left = mid + 1;
}
return res;
}
int getLastIndex(int* arr, int length, int k)
{//二分法find大于k的最后一个数
if (arr == nullptr || length <= 0 || arr[length - 1] <= k)
return -1;
int res = length - 1,left=0,right=length-1;
while (left <= right)//这里的等于号很重要,只有这里等于才能找到第一个
{
int mid = left + (right - left) / 2;
if (arr[mid] < k)
{
res=mid;
left= mid + 1;
}
else
right = mid - 1;
}
return res;
}
7. 奇数在奇数索引上,偶数在偶数索引上 (双指针 odd=1;even=0;和arr[end]做就交换)
主要思路:
- while ((odd<=end)&&(even<=end))
- odd=1;even=0;和arr[end]做就交换,如果是偶数,和arr[even]交换,然后even++
8. 奇数在前,偶数在后 (双指针,前后各一个,各控制奇数和偶数)
主要思路:
- while(前指针<后指针)
- 前面的指针,while到偶数停下,后面的指针,while到奇数停下,然后交换
注意事项:
上面第2步中,while到偶数停下,需要while(begin<end && arr[begin]&1!=0) begin++;
==================================================================
还可以用STL中的partition
bool IsOdd (int i) { return (i%2)==1; }//奇数优先,在左
int main () {
std::vector<int> myvector;
// set some values:
for (int i=1; i<10; ++i) myvector.push_back(i); // 1 2 3 4 5 6 7 8 9
std::vector<int>::iterator bound;//返回的bound是第一个偶数的地址
bound = std::stable_partition (myvector.begin(), myvector.end(), IsOdd);//奇数在左
}
9. 最长可整合子数组长度 (假设i-j满足,for (int i=0...for (int j=i))
题目:可整合:排序后相邻数字差必须是1
输入:5 5 3 4 6 2 3
解释 : 2 3 4 5 6
返回长度: 5
==================================================================
主要思路:
这种题目,暴力解,穷举也就是
for (int i = 0; i < length; i++) {
...<这里才是主要初始化的地方>
for (int j = i; j < length; j++) {
==================================================================
主要改进是在:在不重复的数组里面,如果最大值-最小值=size-1;就说明满足条件,更新此时的res。
如果有重复的,直接break结束此次循环(终止条件放在正式赋值前头)
#include <algorithm>
#include <set>
using namespace std;
int getMax(int arr[], int length) {
if (arr == nullptr || length < 0)return -1;
int res = 0;
set<int> temp;//去重用的,如果重复了直接GG重新构造
//需要记录i-j内的最大值和最小值
for (int i = 0; i < length; i++) {
int max = INT_MIN;//加入include <algorithm>才有
int min = INT_MAX;
temp.clear();//每次都需要重新构建
for (int j = i; j < length; j++) {
if (temp.count(arr[j])) { break; }//如果重复了直接GG重新构造
temp.insert(arr[j]);
max = std::max(max, arr[j]);加入include <algorithm>才有
min = std::min(min, arr[j]);
if ((max - min) == j - i) {//注意是j-i,别写反了
res = std::max(res, j - i + 1);
}
}
}
return res;
}
int main() {
int arr[7] = { 5,5,3,4,6,2,3};
int a = getMax(arr, 7);
return 0;
}
10. 子数组的最大累乘积 归纳法-动态规划 (分析每个位置 i 的可能性,主要是从i-1已知了某些条件的角度出发)
有正有负的数组。
动态规划 分析每个位置 i 的可能性,主要是从 i-1 已知了某些条件的角度出发。大的可分为和i-1有关系和没关系,小的还可细分。i处的可能:
1. 和上一步有关系
- 上一步的max*arr[i] i-1已知了max
- 上一步的min*arr[i] i-1已知了min
2. 和上一步没关系 arr[i] 比如0.1 -1 100
看到需要已知min才行,所以还得分析min,min也是上面的三种可能。
凡是向上面这种需要 先已知 i-1 的某些条件的情况,我们初始化的时候就需要 先把初始化弄成是第一个的时候。其实就是归纳法
int getMax(int arr[], int length) {// 归纳法
if (arr == nullptr || length < 0)return -1;
int res = arr[0];
int min = arr[0];// 先初始化一个min和max,下面p1 p2就可以直接先用,后期再更新,INT_MAX错了;
int max = arr[0]; // INT_MIN;
int p1 = INT_MIN;//第一种情况
int p2 = INT_MIN;//第二种情况
for (int i = 1; i < length; i++) {
//先用着min和max
p1 = max*arr[i];
p2 = min*arr[i];
max = std::max(std::max(p1, p2), arr[i]);
min = std::min(std::min(p1, p2), arr[i]);
res = std::max(res, max);
}
return res;
}
int main() {
int arr[7] = { -2,4,0,3,1,8,-1};
int a = getMax(arr, 7);
return 0;
}
11. 最大子数组
O(N)弄一个当前和,if (cursum <= 0) cursum = arr[i]; else cursum += arr[i];
// 当前和<0,就重置
// 假设是i到j,最粗暴的,还可以用动态归纳法
int getMax(int arr[], int length) {
if (arr == nullptr || length < 0)return -1;
int res = INT_MIN;
int cursum = INT_MIN;
for (int i = 0; i < length; i++) {
if (cursum <= 0)cursum = arr[i];
else cursum += arr[i];
res = std::max(res, cursum);
}
return res;
}
int main() {
int arr[7] = { -2,4,0,3,1,8,-1};
int a = getMax(arr, 7);
return 0;
}
//动态规划
int getMax(int arr[], int length) {
if (arr == nullptr || length < 0)return -1;
int res = arr[0];
int last = arr[0];
int cur = 0;
for (int i = 1; i < length; i++) {
if (last <= 0)cur = arr[i];//相当于f(n)=arr[i];
else cur += arr[i];//f(n)+=arr[i];
last = cur;//更新上一个f(n-1)
res = std::max(res, cur);
}
return res;
}
int main() {
int arr[7] = { -2,4,0,3,1,8,-1};
int a = getMax(arr, 7);
return 0;
}
12. 最大子矩阵
假设最终是 i-j行满足,然后压扁这i-j行[空间复杂度O(N),时间复杂度O(N 3)],变成子数组的问题
int n;
int a[110][110];
int b[110];
int main()
{
while (scanf("%d", &n) != EOF)
{
for (int i = 0; i<n; i++)
for (int j = 0; j<n; j++)
scanf("%d", &a[i][j]);
int Max = INT_MAX;
for (int i = 0; i<n; i++)
{//以i行开始算起,找j行
memset(b, 0, sizeof(b));//每次辅助的数据都需要重新初始化,表示i变了
for (int j = i; j<n; j++)
{
//下面是针对数组b求最大子段和的动态规划算法
int cursum = 0;
for (int k = 0; k<n; k++)
{
b[k] += a[j][k];//至此化成了一维度的问题
cursum += b[k];//
if (cursum<0) cursum = b[k];
if (cursum>Max) Max = cursum;
}
}
}
printf("%d\n", Max);
}
return 0;
}
13. 查找 局部最小值
14. 查找 最小的K个数【Partition or 大根堆小根堆】
主要思想:
- Partition(O(N))
- int topK(int arr[], int length,int start, int end, int k) //相对于普通的排序,多了一个参数k,也正常
- 每次从start-end中随机取出pivot进行Partition,然比较此处pivot和k的关系
- int NumSmall = pivot-start+1 // 记录 <=arr[pivot]的个数
- if(NumSmall ==k) //说明相等,直接返回此刻Partition后的arr的arr[start - pivot]闭区间
- if(NumSmall >k) // 说明<=arr[pivot]的个数 大于100个,将end换成pivot-1,递归topK
- if(NumSmall <k) // 说明<=arr[pivot]的个数 小于100个,将start换成pivot+1,而此刻的k也更新K-NumSmall,递归topK
2.利用大根堆或者小根堆(O(N*logK)适用于海量数据)
首先,这里存在一个背景知识,构建大根堆或者小根堆,此题应用小根堆。
=============================================================
构建大根堆,有两种方式。基于STL
- 1.利用函数和vector定义私有成员变量 ,
vector<int> max;
- 利用push_heap pop_heap的方式结合vector.pop_back()的方法。每次改变vec之后,都进行
//大根堆
max.push_back(num);
push_heap(max.begin(), max.end(), less<T>());
pop_heap(max.begin(), max.end(), less<T>());##堆顶和最后的互换
max.pop_back();##弹出最后一个 上两个结合heapify
- 2.利用set或者multiset<允许重复>和比较器cmp进行构建。
typedef multiset<int,greater<int>> intSet;#直接定义个大根堆
typedef multiset<int,greater<int>>::iterator intSetIter;
intSet& leastNumbers
如果 leastNumbers < k 直接插入,如果大于k,如果大于k,看当前加入的数*iter,
如果小于堆顶的数,删除 erase 堆顶 leastNumbers.begin(),插入该数。
vector<int>::const_iterator iter = data.begin();
for(; iter != data.end(); ++ iter)
{
if((leastNumbers.size()) < k)
leastNumbers.insert(*iter);
else
{
setIterator iterGreatest = leastNumbers.begin();
if(*iter < *(leastNumbers.begin()))
{
leastNumbers.erase(iterGreatest);
leastNumbers.insert(*iter);
}
}
}
=============================================================
回到正题:
// Partition的方法
int getrand(int start, int end) {
srand((unsigned)time(NULL));
return (start + (rand() % (end - start)));
}
int partition(int arr[], int length,int start, int end)
{
int pivot = getrand(0, length - 1);
int small = 0;//比pivot处小的索引
std::swap(arr[pivot], arr[end]);
for (int i = 0; i < length - 1; i++) {
if (arr[i] < arr[pivot]) {
if (small != i)std::swap(arr[small], arr[i]);
small++;
}
}
std::swap(arr[small], arr[end]);//最后small左边都是小于arr【end】的数
return small;
}
int topK(int A[], int length,int low, int high, int k)
{
if (k <= 0)
return -1;
if (low == high)
return low;
//随机返回一个
int pos = partition(A, length, low, high);
int i = pos - low + 1;// 看看前面有多少个数
if (i == k)
return pos; // 返回前k个数的,此刻的0-pos就是前k的小的数
else if (i > k)//如果大于100,把high变pos
return topK(A, length, low, pos, k);
else
return topK(A, length, pos + 1, high, k - i);// 注意这里的k-i
}
//第二种方法,就是遍历一次arr,然后就出来了
15. 查找 N 个数组整体最大的 TopK【还没有写】
16. 查找 第K大的数
其实和14是一样的,partition
17. 查找 超过一半(N/2)的数 (构建一个map,记录res和count)
主要思路:
- 超过一半的数必定是排序好的中位数,可以利用partition找中位数(不写了)
- 每次删除两个不同的数字,最后剩下的数就是那个超过一半的数。
- 删除两个不同的数字,构建一个map,记录res和count,或者像我下面代码写的,分开记录,遍历,如果和arr中的数一样就count++,不一样就--(删除两个不同的数字,体现在这里,没有对此处的值做反应+count-- 相当于减去两个不一样的值)
bool isExist = true;//避免歧义
int get(int arr[], int length) {
if(arr==nullptr||length<0) isExist = false;
int res = arr[0];
int times = 1;
for (int i = 1; i < length; i++) {
if (arr[i] == res)times++;
else if (times == 0) { //如果times已经为0了。,更换res
res = arr[i];
times = 1;
}
else times--;
}
if (isExist)return res;
}
int main()
{
int arr[6] = { 1,2,3,2,2,2 };
int res = get(arr, 6);
return 0;
}
18. 查找 超过N/K次的数 (构建一个map[num,count])k-1
上一题的上级版本,每次删除不同的K个数,最后落下的就是。k-1个key
注意遍历的时候用到erase需要小心,执行语句里直接break
//所有的key都减去一,如果count=1,直接删除掉这个key即可
void alljianone(map<int, int>&input) {
map<int, int>::iterator temp;
for (temp= input.begin(); temp != input.end();)
{
if (input[temp->first] != 1){
input[temp->first]--;
temp++;//
}
else{
input.erase(temp->first);
break;//用到erase了遍历需要小心,直接break
}
}
}
// 表示记录下超过N/k[最多k-1个,所以空间复杂度是O(k)],以map<num,counts>的形式
map<int,int> get(int arr[], int length,int k) {
//
map<int, int> res;
for (int i = 0; i < length; i++) {
if (res.count(arr[i]) != 0)//如果存在这个num
res[arr[i]]++;//count++
else {
//如果不存在,看下key是否已经k-1个了,如果是,那么每个count--;如果不是弄进来,count=1
if (res.size() != k - 1)
res[arr[i]] = 1;
else {
alljianone(res);//每个count--
}
}
}
return res;
}
int main()
{
int arr[6] = { 3,2,3,3,2,2 };
map<int,int> res = get(arr, 6,3);
return 0;
}
19. 需要排序的最短子数组长度 (从右向左遍历【左边界】+从左向右遍历【右边界】)
主要思路:
主体是找两边边界的问题。
- 从右向左遍历,一遍遍历,一遍更新min_value,然后记录比min_value大的最左的位置;
- 从左向右遍历,一遍遍历,一遍更新max_value,然后记录比max_value小的最右的位置;
- 左右边界就都可以得到了。