千里之行,始於足下;代碼還是得自己寫,才能記憶深刻,這是一個需要累積時間的過程,不是說你在圖書館待上一週,看着書就會了的事情。也由此,在此總結數組合矩陣相關的問題,自己寫代碼實現(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小的最右的位置;
- 左右邊界就都可以得到了。