数组专题

1 找数 | 遍历

1.0 梳理

  • 默认情况
    • 无需排序,或者直接使用进行排序
  • 找数任务
    • 出现次数(一次、超过一半、重复)
    • 求和

1.1 数组中只出现过一次的数

链接:https://www.nowcoder.com/questionTerminal/e02fdb54d7524710a7d664d082bb7811

题目描述:
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

#include <unordered_map>

class Solution {
public:
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        unordered_map<int,int> kv;
        for(int i = 0; i < data.size(); i++){
            if(kv.find(data[i]) != kv.end())
                kv[data[i]] += 1;
            else
                kv[data[i]] = 1;
        }
        
        bool is_first = false;
        unordered_map<int,int>::iterator iter;
        for(iter = kv.begin(); iter != kv.end(); iter++){
            if(iter->second == 1){
                if(is_first == false){
                    *num1 = iter->first;
                    is_first = true;
                }
                else{
                    *num2 = iter->first;
                }
            }
        }
    }
};

1.2 (有限范围)数组中重复的数字

链接:https://www.nowcoder.com/questionTerminal/623a5ac0ea5b4e5f95552655361ae0a8

题目描述:
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

class Solution {
public:
    bool duplicate(int numbers[], int length, int* duplication) {
        if(numbers == NULL || length == 0)
            return false;
         
        int *num_cnt = new int[length]{0};
        for(int i= 0; i < length; i++){
            num_cnt[numbers[i]] += 1;
            if(num_cnt[numbers[i]] > 1){
                *duplication = numbers[i];
                return true;
            }
        }
        return false;
    }
};

1.3 数组中出现次数超过一半的数字

链接:https://www.nowcoder.com/questionTerminal/e8a1b01a2df14cb2b228b30ee6a92163

题目描述:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

思路:如果有符合条件的数字,则它出现的次数比其他所有数字出现的次数和还要多。
在遍历数组时保存两个值:一是数组中一个数字,一是次数。遍历下一个数字时,若它与之前保存的数字相同,则次数加1,否则次数减1;若次数为0,则保存下一个数字,并将次数置为1。遍历结束后,所保存的数字即为所求。然后再判断它是否符合条件即可。

问题过于trick,不够通用

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        int len = numbers.size();
        if(len == 0)
            return 0;
         
        int num = numbers[0];
        int cnt = 1;
        for(int i = 1; i < len; i++){
            if(numbers[i] == num)
                cnt++;
            else
                cnt--;
             
            if(cnt == 0){
                num = numbers[i];
                cnt = 1;
            }
        }
         
        // 可能依旧不存在
        cnt = 0;
        for (int i = 0; i < len; i++) {
            if (numbers[i] == num)
                cnt++;
        }
        if (cnt * 2 > len)
            return num;
        return 0;
    }
};

1.4 有序数组去重

去重任务可以理解为,不断查找第n个不一样的数

去重 = 遍历添加1个与之前不一样的值;index标记位重要

1.4.1 重复一次(n=1)

链接:https://www.nowcoder.com/questionTerminal/a519784e89fb40199fbe7993786715b1

题目描述
给定一个已排序的数组,使用就地算法将重复的数字移除,使数组中的每个元素只出现一次,返回新数组的长度。

不能为数组分配额外的空间,你必须使用常熟级空间复杂度的就地算法。

例如,
给定输入数组 A=[1,1,2],
你给出的函数应该返回length=2,A数组现在是[1,2]。

class Solution {
public:
    int removeDuplicates(int A[], int n) {
        if(n <= 1)
            return n;

        int index = 1;
        for(int i = 1; i < n; i++)
        {
            if(A[i] != A[index-1])
                A[index++] = A[i];
        }

        return index;
    }
};

1.4.2 重复两次(n=2)

链接:https://www.nowcoder.com/questionTerminal/567f420f12ed4069b7e1d1520719d409

题目描述:
继续思考题目"Remove Duplicates":

如果数组中元素最多允许重复两次呢?

例如:
给出有序数组 A =[1,1,1,2,2,3],
你给出的函数应该返回length =5, A 变为[1,1,2,2,3].

class Solution {
public:
    int removeDuplicates(int A[], int n) {
        if(n <= 2)
            return n;

        int index = 2;
        for(int i = 2; i < n; i++)
        {
            if(A[i] != A[index-2])
                A[index++] = A[i];
        }
        return index;
    }
};

1.5 和为S的两个数字(递增数组)

链接:https://www.nowcoder.com/questionTerminal/390da4f7a00f44bea7c2f3d19491311b

题目描述:
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

结合问题,利用递增序列性质

class Solution {
public:
    vector<int> FindNumbersWithSum(vector<int> array,int sum) {
        vector<int> res;
        int len = array.size();
 
        int low = 0;
        int high = len - 1;
        while(low < high){
            int cur_sum = array[low] + array[high];
            if(cur_sum == sum){
                res.push_back(array[low]);
                res.push_back(array[high]);
                break; // 递增序列,相差越远乘积越小,因此无需再计算
            }
            else{
                if(cur_sum > sum)
                    high = high - 1; //low后面的数都大于array[low],因此需要high向前
                else
                    low = low + 1; //high前面的数都小于array[high],因此需要low向后
            }
        }
        return res;
    }
};

1.6 三数之和

链接:https://www.nowcoder.com/questionTerminal/291a866d7c904563876b373b0b157dde

题目描述
给出含有n个整数的数组s,找出s中和加起来的和最接近给定的目标值的三个整数。返回这三个整数的和。你可以假设每个输入都只有唯一解。

例如,给定的整数 S = {-1 2 1 -4}, 目标值 = 1.最接近目标值的和为 2. (-1 + 2 + 1 = 2).

先排序,然后左右夹逼,复杂度O(n^2)

这个方法可以推广到k-sum,先排序,然后做k-2 次循环,在最内层循环左右夹逼,时间复杂度是O(max(nlogn; n^(k-1)))

【思路】

  • 两个变量:sum、diff
  • 两个条件判断:1)sum > target ? 2) abs(sum-target) < diff ?
class Solution {
public:
    int threeSumClosest(vector<int>& num, int target) {
        sort(num.begin(), num.end());
        
        int len = num.size();
        
        int sum = num[0] + num[1] + num[len-1];
        int cur_diff = sum - target;
        for(int i = 0; i < len; i++)
        {
            int l = i + 1;
            int r = len - 1;
            while(l < r)
            {
                if(num[i] + num[l] + num[r] < target)
                {
                    if(abs(num[i] + num[l] + num[r] - target) < abs(cur_diff))
                    {
                        sum = num[i] + num[l] + num[r];
                        cur_diff = sum - target;
                    }
                    l++; //循环内类似两数之和,利用递增序列性质
                }
                else if(num[i] + num[l] + num[r] > target)
                {
                    if(abs(num[i] + num[l] + num[r] - target) < abs(cur_diff))
                    {
                        sum = num[i] + num[l] + num[r];
                        cur_diff = sum - target;
                    }
                    r--;
                }
                else
                    return num[i] + num[l] + num[r];
            }
        }
        return sum;
    }
};

2 找数 | 有序 | 二分查找

2.1 数字在排序数组中出现的次数

链接:https://www.nowcoder.com/questionTerminal/70610bf967994b22bb1c26f9ae901fa2

题目描述
统计一个数字在排序数组(从小到大)中出现的次数。

class Solution {
public:
    int BinarySearch(vector<int> data ,int k, int low, int high){
        while(low <= high){
            int mid = (low+high)/2;
            if(data[mid] == k)
                return mid;
            else if(data[mid] < k)
                low = mid + 1;
            else
                high = mid - 1;
        }
        return -1;
    }
    
    int GetNumberOfK(vector<int> data ,int k) {
        if(data.size() == 0)
            return 0;
        
        int len = data.size();
        int index = BinarySearch(data, k, 0, len - 1);
        
        if(index == -1)
            return 0;
        else{
            int cnt = 1;
            for(int i = index - 1; i >= 0; i--){
                if(data[i] == k)
                    cnt++;
                else
                    break;
            }
            for(int j = index + 1; j < len; j++){
                if(data[j] == k)
                    cnt++;
                else
                    break;
            }
            return cnt;
        }
    }
};

2.2 二维数组中的查找(行/列递增)

链接:https://www.nowcoder.com/questionTerminal/abc3fe2ce8e146608e868a70efebf62e

题目描述:
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

错误思考:
先找到行所在的index,再从对应的列找val,即两次二分搜索。错误的原因是列是递增的,无法确定所在行。

class Solution {
public:    
    bool Find(int target, vector<vector<int> > array) {
        if(array.empty())
            return false;
        
        int row = array.size();
        int col = array[0].size();
        
        for(int i = 0; i < col; i++){
        	//注意行递增性质
            if(array[0][i] > target)
                break;
            
            //二分查找
            int low = 0, high = row-1;
            while(low <= high){
                int mid = low + (high - low)/2;
                if(array[mid][i] == target)
                    return true;
                else if(target > array[mid][i])
                    low = mid + 1;
                else
                    high = mid - 1;
            }
        }
        return false;
    }
};

2.3 转动有序数组查找(无重复|有重复)

有重复是更一般的情形,所以两题一套代码即可

一、数组中无重复
【链接】https://www.nowcoder.com/questionTerminal/7cd13986c79d4d3a8d928d490db5d707

【题目描述】
给出一个转动过的有序数组,你事先不知道该数组转动了多少
(例如,0 1 2 4 5 6 7可能变为4 5 6 7 0 1 2).
在数组中搜索给出的目标值,如果能在数组中找到,返回它的索引,否则返回-1。
假设数组中不存在重复项。

二、数组中有重复
【链接】https://www.nowcoder.com/questionTerminal/d942d1aabf5549b0b53af55f1d4432e4

【题目描述】
继续思考题目 “Search in Rotated Sorted Array”:

如果数组种允许有重复元素怎么办?
会影响时间复杂度吗?是怎样影响时间复杂度的,为什么?
编写一个函数判断给定目标值是否在数组中。

【解析】
二分查找法

整个旋转数组分为两部分一定有一部分有序,那么通过判断左边还是右边有序分为两种情况。然后在根据target进行判断上、下边界

不断剪枝有序部分的一半数组,进而达到log(n)复杂度

思维转换:区别于有序数组,low,high仅理解为target所在范围的下标,A[low]与A[high]间不存在严格关系

class Solution {
public:
    int search(int* A, int n, int target) {
        int low = 0, high = n-1;
        while(low <= high){
            int mid = low + (high - low) / 2;
            
            if(A[mid] == target)
                return mid;

            if(A[low] < A[mid]){ //左边有序
                //在有序的前提下,只有如下一种情况说明target在左边,即下标[low,mid-1]
                if(target >= A[low] && target < A[mid]) 
                    high = mid - 1;
                else
                    low = mid + 1;
            }
            else if(A[low] > A[mid]){ //右边有序
                if(target > A[mid] && target <= A[high]) //同上,通过target进行判断
                    low = mid + 1;
                else
                    high = mid - 1;
            }
            else{
                //low == mid,再加上 A[mid] != target(return部分反推), 则low++即可
                //当无无重复情形时,low == mid,那么high == low,或者 high = low + 1
                low++;
            }
            
        }
        return -1;
    }
};

2.4 构建平衡二叉搜索树

【链接】
https://www.nowcoder.com/profile/351750300/codeBookDetail?submissionId=80848906

【题目描述】
给出一个升序排序的数组,将其转化为平衡二叉搜索树(BST).

输入
[-1,0,1,2]
输出
{1,0,2,-1}

【基础概念】

  • 二叉搜索树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

  • 平衡二叉搜索树:叶节点高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。它能在O(log2n)内完成插入、查找和删除操作

  • 满二叉树是完全二叉树的特例,因为满二叉树已经满了,而完全并不代表满。满指的是出了叶子节点外每个节点都有两个孩子,而完全的含义则是最后一层没有满

  • 完全二叉树第i层至多有2(i-1)个节点,共i层的完全二叉树最多有2i-1个节点

/**
 * struct TreeNode {
 *	int val;
 *	struct TreeNode *left;
 *	struct TreeNode *right;
 * };
 */

class Solution {
public:
    TreeNode* toBST(vector<int>& num, int low, int high){
        if(low > high) //重要
            return NULL;

        int len = high - low + 1;
        int mid;
        if(len % 2 == 0) //序列共偶数个值
            mid = low + (high - low) / 2 + 1;
        else
            mid = low + (high - low) / 2;

        TreeNode* root = new TreeNode(num[mid]);
        root -> left = toBST(num, low, mid - 1);
        root -> right = toBST(num, mid + 1, high);

        return root;
    }
    
    TreeNode* sortedArrayToBST(vector<int>& num) {
        if(num.size() == 0)
            return NULL;

        return toBST(num, 0, num.size()-1);
    }
};

3 找数 | 无序 | top k | 排序

3.0 梳理

  • 排序任务包括
    • 递增排序、字符串拼接排序
  • 找数任务进阶
    • 找pair对(找单值、找和都是找一个数)

3.1 最小的K个数(无序数组)

链接:https://www.nowcoder.com/questionTerminal/6a296eb82cf844ca8539b57c23e6e9bf

题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

3.1.1 快速排序

快速排序的地位在业界是不言而喻的,时间复杂度为O(nlogn),且常系数为2。在不考虑极端情况下,速度是很理想的。

理解快速排序:https://www.jianshu.com/p/7631d95fdb0b

class Solution {
public:
	// 确定基准数,利用partition函数得到基准数在一趟排序后的位置。
    int partition(vector<int> &vec, int low, int high)
    {
        int i = low;
        int j = high;

        int index = i;
        int x = vec[index]; //确定基准数

        while ( i < j)
        {
            //从后向前找,找到一个小于基准数的进行交换
            while (j > i && vec[j] >= x) //注意是等于,所以必须是小于基准数的值
                j--;
            if (j > i)
            {
                vec[i] = vec[j]; //第一轮i为index,基准值
                i++;
            }

            //从前向后找,找到一个大于基准数的进行交换
            while(i < j && vec[i] < x)
                i++;
            if(i < j)
            {
                vec[j] = vec[i];
                j--;
            }
        }

        vec[i] = x;

        return i;
    }

    void quick_sort(vector<int> &vec, int low, int high)
    {
        if (low < high)
        {
            int index = partition(vec, low, high);
            quick_sort(vec, low, index-1);
            quick_sort(vec, index+1, high);
        }
    }

    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        int len = input.size();
        if(len < k)
            return res;

		// sort(input.begin(),input.end()); 等价
        quick_sort(input, 0, len-1);

        for(int i = 0; i < k; i++){
            res.push_back(input[i]);
        }
    }
};

3.1.2 堆排序

构建小顶堆(递减序列,最后一个值最小),从后向前取k个,为Top k小值。

图解堆排序:https://www.cnblogs.com/mobin/p/5374217.html

class Solution {
public:
    //堆调整
    void adjustHeap(vector<int>& vec, int i, int len){
        int child = i*2+1; //左孩子
        if(child < len){
             
             //如果右孩子存在且值小于左孩子
            if(child+1 < len && vec[child+1] < vec[child])
                child = child+1;

            if(vec[child] < vec[i]){
                swap(vec[i], vec[child]);
                adjustHeap(vec, child, len); //再看下子节点是否需要调整
            }
        }
    }
    
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        int len = input.size();
        if(len < k)
            return res;

        //初始化小顶堆
        for(int i = len/2-1; i >= 0; i--) //此处是大于等于,且前面是len/2-1
            adjustHeap(input, i, len); 
        
        //保证从后向前k个值递增
        int i;
        for(i = len-1; i > len-1-k; i--){ //此处是大于
            swap(input[0], input[i]);
            adjustHeap(input, 0, i);
        }
        
        for(i = len-1; i > len-1-k; i--)
            res.push_back(input[i]);
        
        return res;
    }
};

直接堆排序job

    void heapSort(vector<int>&input, int length){ //堆排序
        for(int i=length/2-1; i>=0; i--) //初始化堆             
            adjustHeap(input, i, length);
         
         //大顶推,不断将最大值放到最后一个位置,生成递增序列
        for(int i=length-1;i>0;i--){
        	swap(input, 0, i); 
            adjustHeap(input, 0, i);
        }
    }

3.1.3 归并排序

【基本思想】
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题**分(divide)成一些小的问题然后递归求解,而治(conquer)**的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

分而治之

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

class Solution {
public:
	// 将有二个有序数列vec[first…mid]和vec[mid…last]合并
    void merge_array(vector<int> &vec,int first,int mid,int last,vector<int> &tmp)
    {
        int i = first, j = mid+1;
        int k = 0;
        
        while (i <= mid && j <= last){
            if(vec[i] < vec[j])
                tmp[k++] = vec[i++];
            else
                tmp[k++] = vec[j++];
        }
        
        //如果前半段没有遍历完
        while (i <= mid)
            tmp[k++] = vec[i++];

		//如果后半段没有遍历完
        while(j <= last)
            tmp[k++] = vec[j++];

        //将排好序的tmp赋值给vec
        for (i = 0;i < k; i++)
            vec[first+i] = tmp[i]; //这里很重要,是first+i
     }

	// 分治法
    void merge_sort(vector<int>& vec, int first, int last, vector<int>& tmp)
    {
        if (first < last)
        {
            int mid = first + (last - first)/2;
            //这里与快排有两点不同
            //1)快排是先xxx,在递归;归并排序是先递归在xxx,递归排序是典型的分治策略
            //2)mid边界;快排是mid-1与mid+1;归并排序是mid与mid+1。同样与两者思想有关
            merge_sort(vec,first,mid,tmp);   //左边排好序
            merge_sort(vec,mid+1,last,tmp);  //右边排好序
            merge_array(vec,first,mid,last,tmp);  //再将两个有序数列排序
        }
    }
    
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        int len = input.size();
        if(len < k)
            return res;

        vector<int> tmp(len);
        
        merge_sort(input, 0, len-1, tmp);

        for(int i = 0; i < k; i++)
            res.push_back(input[i]);
        
        return res;
    }
};

3.2 数组中的逆序对(结合归并排序)

链接:https://www.nowcoder.com/questionTerminal/96bd6684e04a44eb80e6a68efc0ec6c5

题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

class Solution {
public:
    long long merge_sort_core(vector<int>& vec,int first,int last,vector<int>& tmp)
    {
        if (first < last)
        {
            int mid = first + (last - first)/2;
            long long left;
            long long right;
            left = merge_sort_core(vec, first, mid, tmp);   //左边排好序
            right = merge_sort_core(vec, mid+1, last, tmp);  //右边排好序
            
            int i = first, j = mid+1;
            int k = 0;
            long long cnt = 0;

            while (i <= mid && j <= last){
                if(vec[i] < vec[j])
                    tmp[k++] = vec[i++];
                else{
                    tmp[k++] = vec[j++];
                    cnt += mid - i + 1; //vec[j]和前面每一个数都能组成逆序数对。前面个数为mid-i+1,j就是一个,所以逆序对个数为(mid-i+1)*1
                }
            }

            while (i <= mid)
                tmp[k++] = vec[i++];

            while(j <= last)
                tmp[k++] = vec[j++];

            //将排好序的tmp赋值给vec
            for (i = 0;i < k; i++)
                vec[first+i] = tmp[i];
            
            return left + right + cnt;
        }
        return 0; //重要
    }
    
    int InversePairs(vector<int> data) {
        int len = data.size();
        if(len == 0)
            return 0;
        
        vector<int> tmp;
        for(int i = 0; i < len; i++)
            tmp.push_back(data[i]);
        
        long long cnt = merge_sort_core(data, 0, len-1, tmp);
        return cnt%1000000007;
    }
};

3.3 把数组排成最小的数(字符串)

链接:https://www.nowcoder.com/questionTerminal/8fecd3f8ba334add803bf2a06af1b993

题目描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

  • 解题思路:
    • 先将整型数组转换成String数组,然后将String数组排序,最后将排好序的字符串数组拼接出来。关键就是制定排序规则。
  • 排序规则如下:
    • 若ab > ba 则 a > b,
    • 若ab < ba 则 a < b,
    • 若ab = ba 则 a = b;
  • 解释说明:
    • 比如 “3” < "31"但是 “331” > “313”,所以要将二者拼接起来进行比较
class Solution {
public:
    string PrintMinNumber(vector<int> numbers) {
        int len = numbers.size();
        if(len == 0)
            return "";

        sort(numbers.begin(), numbers.end(), cmp);
        
        string res;
        for(int i = 0; i < len; i++){
            res += to_string(numbers[i]);
        }
        return res;
    }
    static bool cmp(int a, int b){
        string A = to_string(a) + to_string(b);
        string B = to_string(b) + to_string(a);
        return A < B;
    }
};

3.4 两个有序数组中第k小数(输入|输出流)

链接:https://www.nowcoder.com/questionTerminal/b933e6a7924c44388fc08e807945f6c7

类似题型:Leetcode 4,两个有序数据的中位数

题目描述
给定两个有序(递增)数组arr1和arr2,再给定一个整数K,返回所有数中第K小的数。

[要求]
如果arr1的长度为N,arr2的长度为M,时间复杂度请达到O(log(minN,M)),额外空间复杂度O(1)。

解析:
充分利用数组有序信息,借鉴二分查找的思想,时间复杂度为O(log(M+N))

#include <bits/stdc++.h>

using namespace std;

int findKth(vector<int> a, vector<int> b, int k)
{
    // 总是让A的大小小于B
    if(a.size() > b.size())
        return findKth(b, a, k);

    //终止条件
    if(a.size() == 0)
        return b[k - 1];
    if(k == 1)
        return min(a[0], b[0]);

    int m = a.size();
    int n = b.size();

    int i = min(m, k / 2); // 如果大小不足k/2,取自身大小
    int j = k - i;

    if(a[i - 1] > b[j - 1])
        return findKth(a, vector<int>(b.begin() + j, b.end()), k - j);
    else if(a[i - 1] < b[j - 1])
        return findKth(vector<int>(a.begin() + i, a.end()), b, k - i);
    else
        return a[i-1];
}

int main(){
    vector<int> a,b;
    int a_len, b_len, k, x;
    cin >> a_len >> b_len >> k;
 
    for(int i = 0; i < a_len; i++){
        cin >> x;
        a.push_back(x);
    }
    
    for(int j = 0; j < b_len; j++){
        cin >> x;
        b.push_back(x);
    }
    
    int target = findKth(a, b, k);
    cout << target;
    
    return 0;
}

3.5 合并两条有序数组

链接:https://www.nowcoder.com/questionTerminal/89865d4375634fc484f3a24b7fe65665

题目描述:
给出两个有序的整数数组A和B,请将数组B合并到数组A中,变成一个有序的数组

注意:
可以假设A数组有足够的空间存放B数组的元素,A和B中初始的元素数目分别为m和n

class Solution {
public:
    void merge(int A[], int m, int B[], int n) {
        if(m > 0 && n >0){
            if(A[m-1] > B[n-1]){
                A[m+n-1] = A[m-1];
                merge(A, m-1, B, n);
            }
            else{
                A[m+n-1] = B[n-1];
                merge(A, m, B, n-1);
            }
        }
        else{
            // 如果是m大于0,A数组无需处理
            if(n > 0)
                for(int i = n-1; i >= 0; i--)
                    A[i] = B[i];
        }
    }
};

4 调序

除在递增排序之外,按照一定规则调整数据位置

4.1 调整数组顺序使奇数位于偶数前面

链接:https://www.nowcoder.com/questionTerminal/beb5aa231adc45b2a5dcc5b62c93f593

题目描述:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

解析:
相对位置不变—>保持稳定性;奇数位于前面,偶数位于后面 —>存在判断,挪动元素位置;

class Solution {
public:
    void reOrderArray(vector<int> &array) {
        int len = array.size();
        if(len <= 1)
            return;

        int index = 0;
        vector<int> after_vec;
        for(int i = 0; i < len; i++){
            if(array[i]%2 == 1){
                array[index] = array[i];
                index++;
            }
            else
                after_vec.push_back(array[i]);
        }

        for(int i = 0; i < after_vec.size(); i++){
            array[index] = after_vec[i];
            index++;
        }
    }
};

4.2 之字形打印矩阵(二维数组)

链接:https://www.nowcoder.com/questionTerminal/7df39c7556424eada267d8f793961a1e

题目描述:
对于一个矩阵,请设计一个算法,将元素按“之”字形打印。具体见样例。

给定一个整数矩阵mat,以及他的维数nxm,请返回一个数组,其中元素依次为打印的数字。

测试样例:
[[1,2,3],[4,5,6],[7,8,9],[10,11,12]],4,3
返回:[1,2,3,6,5,4,7,8,9,12,11,10]

class Printer {
public:
    vector<int> printMatrix(vector<vector<int> > mat, int n, int m) {
        vector<int> res;
        if(mat.empty())
            return res;
        
        int row = mat.size();
        int col = mat[0].size();
        
        int i,j;
        for(i = 0; i < row; i++){
            if(i%2 == 0){
                //正序
                for(j = 0; j < col; j++)
                    res.push_back(mat[i][j]);
            }
            else{
                //反序
                for(j = col-1; j >= 0; j--)
                    res.push_back(mat[i][j]);
            }
        }
        return res;
    }
};

4.3 顺时针打印矩阵(二维数组)

链接:https://www.nowcoder.com/questionTerminal/9b4c81a02cd34f76be2659fa0d54342a

题目描述
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

解析:
一圈一圈遍历
再每一篇遍历时,注意边界,避免重复遍历

class Solution {
public:
    vector<int> printMatrix(vector<vector<int>> matrix) {
        int row = matrix.size();
        int col = matrix[0].size();

        vector<int> result;
        if(row==0 || col==0)
            return result;

        //矩阵两角、四个点信息初始化
        int left=0, right=col-1, top=0, bottom=row-1;

        while(left<=right && top<=bottom){
            //一层循环,表示遍历一圈

            //从左往右
            for(int i=left; i<=right; i++)
                result.push_back(matrix[top][i]);

            //从上到下
            if(top < bottom) //边界:防止只有一行的情况
                for(int i=top+1; i<=bottom; i++)
                    result.push_back(matrix[i][right]);

            //从右往左
            if(top<bottom && left<right) //边界:防止只有一行、一列的情况
                for(int i=right-1; i>=left; i--)
                    result.push_back(matrix[bottom][i]);

            //从下到上
            if(top+1<bottom && left<right) //边界:防止只有两行、一列的情况
                for(int i=bottom-1; i>=top+1; i--)
                    result.push_back(matrix[i][left]);

            left++; right--; top++; bottom--;
        }
        return result;
    }
};

5 排列组合 | DFS

  • 全排列:n个数据进行全排列,可得到n!个结果
  • 组合:n个数据任选k个(k < n),n!/((n-k)! * k!)

5.1 全排列

【链接】
https://www.nowcoder.com/profile/351750300/codeBookDetail?submissionId=80668722

【题目描述】
给出一组数字,返回该组数字的所有排列

例如:
[1,2,3]的所有排列如下
[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2], [3,2,1].

解析(递归法):
设一组数p = {r1, r2, r3, … ,rn}, 全排列为perm§,pn = p – {rn}。则perm§ = r1perm(p1), r2perm(p2), r3perm(p3), … , rnperm(pn)。当n = 1时perm(p} = r1。

注意是否字典序的区别

class Solution {
public:
    void permutation(vector<vector<int> > &res,vector<int> &num,int s){
        if(s == num.size()-1)
            res.push_back(num);
        else{
            for(int i = s; i < num.size(); i++){
            	// sort(nums.begin()+s, nums.end()); //加上则输出字典序
                swap(num[s], num[i]);  //for循环将start~end中的每个数放到start位置中去
                permutation(res, num, s+1); //假设start位置确定,那么对start+1~end中的数继续递归
                swap(num[s], num[i]); //回溯
            }
        }          
    }
    
    vector<vector<int> > permute(vector<int> &num) {
        vector<vector<int> > res;
        permutation(res, num, 0);
        return res;
    }
};

5.2 无重复全排列

【链接】https://www.nowcoder.com/questionTerminal/a43a2b986ef34843ac4fdd9159b69863

【题目描述】
给出一组可能包含重复项的数字,返回该组数字的所有排列
例如;
[1,1,2]的排列如下:
[1,1,2],[1,2,1], [2,1,1].

【解析】
理解for 循环中两个 if 的剪枝

由于递归的 for 都是从0开始,要避免重复使用数字,则每个 nums 中的数字在全排列中只能使用一次(当然这并不妨碍 nums 中存在重复数字),具体使用数字就靠 visited 数组来保证,这就是第一个 if 剪枝的意义所在。

关键来看第二个 if 剪枝的意义,这里说当前数字和前一个数字相同,且前一个数字的 visited 值为0的时候,必须跳过。这里的前一个数 visited 值为0,并不代表前一个数字没有被处理过,也可能是递归结束后恢复状态时将 visited 值重置为0了

【删除函数】
res.erase(unique(res.begin(),res.end()),res.end());

class Solution {
public:
    void permuteUnique(vector<int>& nums, int start, vector<vector<int> >& res, vector<int>& path, vector<bool>& visited) {
        if(start == nums.size()) {
            res.push_back(path);
            return;
        }
        
        for(int i = 0; i < nums.size(); i++) {
            if(visited[i])
                continue; 
            if(i > 0 && nums[i] == nums[i - 1] && visited[i -1] == false)
                continue;
            
            path.push_back(nums[i]);
            visited[i] = true;
            permuteUnique(nums, start + 1, res, path, visited); //DFS思想
            path.pop_back();
            visited[i] = false;
        }
    }

    vector<vector<int> > permuteUnique(vector<int> &num) {
        int n = num.size();
        
        vector<vector<int> > res;
        vector<int> path; // 临时保存当前的排列元素
        vector<bool> visited(n, false); // 标记当前元素是否已经在临时排列中
        sort(num.begin(), num.end());
        
        permuteUnique(num, 0, res, path, visited);
        
        return res;
    }
};

5.3 下一个排列(字典序)

链接:https://www.nowcoder.com/questionTerminal/f0069cfcd42649e3b6b0c759fae8cde6

题目描述:
实现函数next permutation(下一个排列):将排列中的数字重新排列成字典序中的下一个更大的排列。将排列中的数字重新排列成字典序中的下一个更大的排列。

如果不存在这样的排列,则将其排列为字典序最小的排列(升序排列)
需要使用原地算法来解决这个问题,不能申请额外的内存空间

下面有机组样例,左边是输入的数据,右边是输出的答案
1,2,3→1,3,2
3,2,1→1,2,3
1,1,5→1,5,1

【题解】
例如:1 2 3 7 6 5 1

  1. 从后往前找到不满足递增排序的点
  2. 如果不存在这样的点证明数组是非递增的,直接reverse,然后返回即可
  3. 如果找到,例如上例中的7,则重新从后往前寻找大于3的第一个数,即5
  4. 交换3和5的位置,然后将后面的数组升序排列,即可得到结果
class Solution {
public:
    void nextPermutation(vector<int> &num) {
        if(num.size() <= 1)
            return;
        
        // 从后往前先找到不满足递增排序的点.当存在点时,对应下标为i-1;不存在下标为0
        int i = num.size() - 1;
        while(i > 0 && num[i-1] >= num[i])
            i--;
        
        if(i == 0)
            //当从后向前为递增序列,直接倒序即可
            reverse(num.begin(), num.end());
        else{
            // swap这个点与后面第一个(从后向前)大于此的数
            int j = num.size() - 1;
            while(num[j] <= num[i-1])
                j--;
            swap(num[i-1], num[j]);
            
            //然后对后面升序排列
            sort(num.begin()+i, num.end()); //注意是加i
        }
    }
};

5.4 第k个全排列

【链接】https://www.nowcoder.com/questionTerminal/186c35e87f7b45beaa556dbbf670759e

【题目描述】
集合[1,2,3,…,n]一共有n!种不同的排列
按字典序列出所有的排列并且给这些排列标上序号
我们就会得到以下的序列(以n=3为例)
“123”
“132”
“213”
“231”
“312”
“321”
现在给出n和k,请返回第k个排列
注意:n在1到9之间

与其他全排列题可结合
【解法一】直接调用k次下一个排列,也可以解(时间复杂度高一些)
【解法二】按字典序得到全部全排列,取第k个(内存消耗大)

5.4.1 解法三:两层遍历逐个确认

【解题思路】

n = 3,k = 3的时候
nums=[1,2,3]的数组全排列为

  • 123
  • 132 — 1 * 2!
  • 213
  • 231 — 2 * 2!
  • 312
  • 321 ----3 * 2!

不断循环:
如果(i-1)*(n - 1)! < k <= i * (n - 1)!
那个第一个数字就应该为nums[i - 1]
将这个数剔除nums中:nums.erase(nums.begin() + (i - 1));
k = k - (i - 1) * ( n - 1)!

class Solution {
public:
    string getPermutation(int n, int k) {
        string res = "";
        
        vector<int> nums;
        //将所有的数字都放在nums这个数组中
        for(int i = 1; i <= n; i++)
            nums.push_back(i);
        
        //两层循环,内层循环遍历找到第j个位置的值
        for(int j = 0; j < n; j++){
            int len = nums.size();
            //k如果在(i-1)* (len - 1)!到i * (len - 1)!
            //那么str += nums[i - 1]; k -= (i - 1) * (len - 1)!
            for(int i = 1; i <= len; i++){
                int v = getMul(len - 1);
                if((i - 1)*v < k && k <= i * v){
                    char c = '0' + nums[i - 1]; // char c = '0' + 1 = '1'
                    res += c;
                    nums.erase(nums.begin() + (i - 1));
                    k -= (i - 1) * v;
                    break;
                }
            }
        }  
        return res;
        
    }
    //计算阶乘的函数
    int getMul(int n){
        int res = 1;
        for(int i = 2; i <= n; i++)
            res *= i;
        return res;
    }
};

5.4.2 解法四:康托编码

【解题思路】
采用康托编码的思路。其实就是康托展开的逆过程。康托展开用来求某个全排列数是第几小的数,也就是当这些数按顺序排时第几个数。

过程如下:比如求321 是 第几小的,可以这样来想:小于3的数有1和2 两个,首位确定之后后面两位有2!中情况,所以共有2*2!=4种。

小于2的数只有一个1,所以有11!=1种情况,最后一位是1,没有比一小的数,所以是00!=0

综上:小于321的数有4+1=5个,所以321是第六小的数。

逆过程就是已知这个数是第k个数,求这个数是多少,当然是知道n的值的。

第k个数就是有k-1个数比这个数小。

所以就是 k-1=an*(n-1)!+an-1*(n-2)!+…+a1*0!;

再举一个例子:

如何找出第16个(按字典序的){1,2,3,4,5}的全排列?

首先用16-1得到15

用15去除4! 得到0余15

用15去除3! 得到2余3

用3去除2! 得到1余1

用1去除1! 得到1余0

有0个数比它小的数是1,所以第一位是1

有2个数比它小的数是3,但1已经在之前出现过了所以是4

有1个数比它小的数是2,但1已经在之前出现过了所以是3

有1个数比它小的数是2,但1,3,4都出现过了所以是5

最后一个数只能是2

所以排列为1 4 3 5 2

class Solution {
public:
    string getPermutation(int n, int k) {
        vector<int> num;
        int amount = 1;
        for(int i = 1; i <= n; ++i){
            num.push_back(i);
            amount *= i; //节约单独计算阶层时间
        }
        
        string res;
        int i;
        k = k - 1;
        while(n > 0){ //n理解为第n阶层,较为好理解
            amount /= n;
            i = k / amount;
            k = k % amount;
            res.push_back('0' + num[i]); //char c = '0' + 1 = '1'
            num.erase(num.begin()+i); //避免重复判断位置
            n = n - 1;
        }
        return res;
    }
};

5.5 k - n组合

【链接】https://www.nowcoder.com/questionTerminal/4d0a110416d84c7f9454d0da53ab2da1

【题目描述】
给出两个整数n和k,返回从1到n中取k个数字的所有可能的组合

例如:
如果n=4,k=2,结果为
[↵ [2,4],↵ [3,4],↵ [2,3],↵ [1,2],↵ [1,3],↵ [1,4],↵]

解析
运用了递归、回溯,当凑够一组时,就回退回去,pop掉一个数,在进行组合。在tmp里,2进出,3进出,用了回溯的思想,进行分别与1组合
在这里插入图片描述

class Solution {
public:
    void DFS(vector<vector<int> >& res, vector<int>& path, int n, int start, int k){
        if(k == 0){
            res.push_back(path);
            return; //强制回溯,无需继续后续遍历,只取k的差异;与将for循环条件中n改成n-k+1一样,后者不易理解
        }
        else{
            //如何理解n-k+1
            for(int i = start; i <= n; i++){
                path.push_back(i);
                DFS(res, path, n, i+1, k-1); //i+1作为下一步的start //注意是i+1,不是start+1
                path.pop_back();
            }
        }
    }
    
    vector<vector<int> > combine(int n, int k) {
        vector<vector<int> > res;
        if(k > n || n == 0)
            return res;
        
        vector<int> path;
        DFS(res, path, n, 1, k);
        return res;
    }
};

5.6 组合求和(只用一次,结果去重、递增)

【链接】https://www.nowcoder.com/questionTerminal/75e6cd5b85ab41c6a7c43359a74e869a

【题目描述】
给出一组候选数C和一个目标数T,找出候选数中起来和等于T的所有组合。
C中的每个数字在一个组合中只能使用一次。

注意:

  • 题目中所有的数字(包括目标数T)都是正整数
  • 组合中的数字 (a 1, a 2, … , a k) 要按非递增排序 (ie, a 1 ≤ a 2 ≤ … ≤ a k).
  • 结果中不能包含重复的组合

例如:给定的候选数集是[10,1,2,7,6,1,5],目标数是8
解集是:
[1, 7]
[1, 2, 5]
[2, 6]
[1, 1, 6]

【题解】
DFS,只需要前期对candidates的排序,后期不需要对返回结果排序.只需要在同一层DFS的时候检查相邻元素是否相同,避免从相同元素在同一层开始DFS就可以

class Solution {
public:
    void DFS(vector<vector<int> >& res, vector<int> &num, vector<int> &path, int target, int start){
        if(target == 0){
            res.push_back(path);
            return; //当不是全排列时加上
        }
        else{
            for(int i = start; i < num.size(); i++){
                //去重
                if(i > start && num[i] == num[i-1]) //注意是i>start
                    continue;
                
                path.push_back(num[i]);
                DFS(res, num, path, target-num[i], i+1);
                path.pop_back();
            }
        }
    }
    
    vector<vector<int> > combinationSum2(vector<int> &num, int target) {
        vector<vector<int> > res;
        if(num.empty())
            return res;
        
        sort(num.begin(), num.end());
        
        vector<int> path;
        DFS(res, num, path, target, 0);
        return res;
    }
};

5.7 组合求和(可重复使用,结果去重、递增)

【链接】https://www.nowcoder.com/questionTerminal/ff509107d96148778f6a14c885d74ace

【题目描述】
给出一组候选数C和一个目标数T,找出候选数中加起来和等于T的所有组合。
C中的数字在组合中可以被无限次使用

注意:

  • 题目中所有的数字(包括目标数T)都是正整数
  • 你给出的组合中的数字 (a 1, a 2, … , a k) 要按非递增排序 (ie, a 1 ≤ a 2 ≤ … ≤ a k).
  • 结解集中不能包含重复的组合

例如:给定的候选数集是[2,3,6,7],目标数是7
解集是:
[7]
[2, 2, 3]

class Solution {
public:
    void DFS(vector<vector<int> >& res, vector<int> &candidates, vector<int> &path, int target, int start){
        if(target == 0){
            res.push_back(path);
            return; //当不是全排列时加上
        }
        else{
            for(int i = start; i < candidates.size(); i++){
                if(candidates[i] <= target){ //注意是小于等于
                    path.push_back(candidates[i]);
                    DFS(res, candidates, path, target-candidates[i], i); //可重复,将原i+1改为i即可
                    path.pop_back();
                }
            }
        }
    }

    vector<vector<int> > combinationSum(vector<int> &candidates, int target) {
        vector<vector<int> > res;
        if(candidates.empty())
            return res;
        
        sort(candidates.begin(), candidates.end());
        
        vector<int> path;
        DFS(res, candidates, path, target, 0);
        return res;
    }
};

6 贪心 | 动态规划

6.1 (贪心)Candy(糖果分配)

链接:https://www.nowcoder.com/questionTerminal/74a62e876ec341de8ab5c8662e866aef

题目描述:
有N个小朋友站在一排,每个小朋友都有一个评分
你现在要按以下的规则给孩子们分糖果:
每个小朋友至少要分得一颗糖果
分数高的小朋友要他比旁边得分低的小朋友分得的糖果多
你最少要分发多少颗糖果?

解题思路:
从左往右遍历,保证当后面的分高于前面时,糖果数+1;
从右往左遍历,弥补一次遍历未覆盖的情况,如评分序列:[2,5,3,2],第一次遍历后得分为[1,2,1,1],但是3>2(糖果数却相同)且5>3,因此最终评分为[1,3,2,1]

class Solution {
public:
    int candy(vector<int>& ratings) {
        int len = ratings.size();
        if(len <= 1)
            return len; 

        vector<int> v(len,1); //初始将每个孩子的糖果数都设为1

        //从左向右扫描,保证这一方向上分数更大的糖果更多
        for(int i = 1; i < len; i++){
            //后面大于前面
            if(ratings[i] > ratings[i-1])
                v[i] = v[i-1] + 1;
        }

        //从右向左扫描,保证这一方向上分数更大的糖果更多
        for(int j = len-1; j >= 1; j--){
            //前面大于后面,并且糖果数目前小于或等于
            if(ratings[j-1] > ratings[j] && v[j-1] <= v[j])
                v[j-1] = v[j] + 1;
        }
        
        int sum = 0;
        for(int i = 0; i < len; i++)
            sum += v[i];
        return sum;
    }
};

6.2 (贪心)Gas Station(加油站)

链接:https://www.nowcoder.com/questionTerminal/3b1abd8ba2e54452b6e18b31780b3635

加油站,与4类似,可以对比着看,属于贪心方法

题目描述:
环形路上有n个加油站,第i个加油站的汽油量是gas[i].
你有一辆车,车的油箱可以无限装汽油。从加油站i走到下一个加油站(i+1)花费的油量是cost[i],你从一个加油站出发,刚开始的时候油箱里面没有汽油。

求从哪个加油站出发可以在环形路上走一圈。返回加油站的下标,如果没有答案的话返回-1。

注意:
答案保证唯一。

【解题思路】
其实本质就是:起点将路径分为前后两段

  • 前段总的余量为负,即油不够用,要想有解,那么后段油量应该为正,此时才可能有解,我们要做的就是找到这个分割点作为起点
  • 反之,如果前段就为正了,那么显然可以直接选择前面的点为起点
  • 如果整段加起来都是负的,那么无解。
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int index = -1;
        int total = 0, sum = 0;
        for(int i = 0; i < gas.size(); i++){
            sum += gas[i] - cost[i]; //本次消耗
            total += gas[i] - cost[i]; //总消耗,用于判断整体是否有解

            if(sum < 0){
                index = i; //记录解的位置
                sum = 0;
            }
        }

        return total >= 0 ? index+1 : -1; //注意是index+1
    }
};

6.3 滑动窗口的最大值(双端队列)

链接:https://www.nowcoder.com/questionTerminal/1624bc35a45c42c0bc17d17fa0cba788

题目描述
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

使用下标是为了便于统计间隔,如i-s.front()+1
使用双端队列deque,为了从前、后都能pop
想要O(n)复杂度,就需要在遍历过程中操作,并思考需要维护的内存
deque s.size()的作用是防止,s为空时取s.back()或者s.front()

class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size)
    {
        vector<int> res;
        if(size < 1 || num.size() < 1)
            return res;
        
        deque<int> s; //记录下标
        s.push_back(0);
        for(unsigned int i=1; i<num.size(); ++i){

            //1 维护s为单调递减序列
            while(s.size() && num[s.back()] <= num[i])
                s.pop_back();

            s.push_back(i);
            if(i-s.front()+1 > size) //2 当s单调递减且超过size时,走这条路径
                s.pop_front();

            //3 开始输出数据
            if(i+1 >= size)
                res.push_back(num[s.front()]);
        }
        return res;
    }
};

6.4 最长序列(无序数组)

6.4.1 递增 | 下标连续 | 简单题

链接:https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/

题目描述:
给定一个未经排序的整数数组,找到最长且连续的的递增序列,并返回该序列的长度。

示例 1:

输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
示例 2:

输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        if(nums.size() <= 1)
            return 1;

        int max_len = 1;
        int cur_len = 1;
        for(int i = 1; i < nums.size(); i++){
            if(nums[i] > nums[i-1])
                cur_len += 1;
            else
                cur_len = 1;
            
            if(cur_len > max_len)
                max_len = cur_len;
        }
        return max_len;
    }
};

6.4.2 (动态规划)递增 | 不改变序 | 中等题

类似线性插值,下标类比为投放速度/bid

一、最长递增子序列A(只返回长度)

【链接】https://www.nowcoder.com/questionTerminal/06dbca9614084e9dba9753f99629595c

【题目描述】
给定一个长度为N的数组,找出一个最长的单调自增子序列(不一定连续,但是顺序不能乱)

例如:给定一个长度为8的数组A{1,3,5,2,4,6,7,8},则其最长的单调递增子序列为{1,2,4,6,7,8},长度为6.

【输入描述】

第一行包含一个整数T,代表测试数据组数

对于每组测试数据:
N-数组的长度

a1 a2 … an (需要计算的数组)

保证:

1<=N<=3000,0<=ai<=MAX_INT.

【输出描述】

对于每组数据,输出一个整数,代表最长递增子序列的长度。

【整体思路】
动态规划题,一般先写出问题的最优子结构的关系式,如下:

res[j] = max{ res[i]+1 | num[i] < num[j], 0 =< i < j } //不断累积
其中,num[i]表示数组下标为i的数,res[i]表示以a[i]为结尾的递增序列的长度。

很明显,数组的最长递增子序列的长度为,res[0]到res[n]中的最大值

详解:https://www.jianshu.com/p/b3580d3e4dab

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int LIS(vector<int> num){
    int len = num.size();
    if(len <= 1)
        return len;
    
    vector<int> res(len,1); //每一个值记录对应下标结尾的最长递增序列的长度,初始值为1
    int max_len = 1;
    for(int i = 1; i < len; i++){
        for(int j = 0; j < i; j++){
            if(num[i] > num[j]){
                //用max解决如1,3,2,4序列中2这个值的情况
                res[i] = max(res[i], res[j]+1);
                max_len = max(res[i], max_len);
            }
        }
    }
    return max_len;
}

int main(){
    int T; //测试数据组数
    cin >> T;
    for(int i = 0; i < T; i++){
        int num_len;
        cin >> num_len;
        
        vector<int> vec(num_len);
        for(int j = 0; j < num_len; j++)
            cin >> vec[j];
        
        int max_LIS_len = LIS(vec);
        cout << max_LIS_len << endl;
    }
    return 0;
}

二、最长递增子序列B(输出数组)

链接:https://www.nowcoder.com/questionTerminal/97b1f5397e054bf895c82ccf2993fd60

  • 注意vector二维数组初始化
  • 测试用例都通过,直接提交报错,暂时不到原因

思路:与一方案类似,在第二层循环时,根据具体情况push_back。第一个值对应的最长序列需要开始就初始化。实际不需要保存每一个数组的结果,在第二层循环结束上,与当前最佳值对比,保留最优即可

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

void output_vec(vector<int> vec){
    for(int i = 0; i < vec.size(); i++)
        cout << vec[i] << " ";
    cout << endl;
}

vector<int> LIS(vector<int> num){
    int len = num.size();
    if(len <= 1)
        return num;
    
    vector<vector<int>> res_vec(len, vector<int>()); //记录每个下标对应的最长递增序列
    res_vec[0].push_back(num[0]);
        
    vector<int> res(len,1); //每一个值记录对应下标结尾的最长递增序列的长度,初始值为1
    for(int i = 1; i < len; i++){
        for(int j = 0; j < i; j++){
            if(num[i] > num[j]){
                //用max解决如1,3,2,4序列中2这个值的情况
                //res[i] = max(res[i], res[j]+1);
                if(res[j]+1 > res[i]){
                    res_vec[i].push_back(num[j]);
                    res[i] = res[j]+1;
                }
            }
        }
        res_vec[i].push_back(num[i]); //加上本身
    }
    
    int max_len = res[0], max_index = 0;
    for(int k = 1; k < len; k++){
        if(res[k] > max_len){
            max_len = res[k];
            max_index = k;
        }
    }
    return res_vec[max_index];
}

int main(){
    int T; //测试数据组数
    cin >> T;
    for(int i = 0; i < T; i++){
        int num_len;
        cin >> num_len;
        
        vector<int> vec(num_len);
        for(int j = 0; j < num_len; j++)
            cin >> vec[j];
        
        vector<int> res = LIS(vec);
        for(int k = 0; k < res.size(); k++)
            cout << res[k] << " ";
        cout << endl;
    }
    return 0;
}

6.4.3 可改变序 | +1递增(值连续) | 较难题

链接:https://www.nowcoder.com/questionTerminal/57d83a2501164168841c158a7535b458

题目描述:
给定一个无序的整数类型数组,求最长的连续元素序列的长度。

例如:
给出的数组为[100, 4, 200, 1, 3, 2],
最长的连续元素序列为[1, 2, 3, 4]. 返回这个序列的长度:4

你需要给出时间复杂度在O(n)之内的算法

解题思路:
用散列表,首先将数字都映射到散列表上,然后,对于每个数字,找到后就删除,然后向两边同时搜,只要搜到了就删除,最后求出长度。哈希表搜是O(1),因为每个数字只会添加一次,删除一次,所以复杂度是O(n)

#include <unordered_set>

class Solution {
public:
    int longestConsecutive(vector<int>& num) {
        unordered_set<int> st(num.begin(), num.end()); //去重字典
        
        int max_len = 0;
        for(int i =0; i < num.size(); i++)
        {
            int l = num[i];
            int r = num[i];
            st.erase(num[i]);
            
            //递归向+1方向搜索
            while(st.find(r+1) != st.end()){
                //便于理解,更简洁的写法是st.erase(++r);
                st.erase(r+1); 
                r = r+1;
            }
            
            //递归向-1方向搜索
            while(st.find(l-1) != st.end()){
                st.erase(l-1);
                l = l-1;
            }
            max_len = max(max_len, r-l+1);
        }
        
        return max_len;
    }
};

6.4.4 (动态规划)连续子数组的最大和

链接:https://www.nowcoder.com/questionTerminal/459bd355da1549fa8a49e350bf3df484

题目描述:
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

问题解析:
使用动态规划
F(i):以array[i]为末尾元素的子数组的和的最大值,子数组的元素的相对位置不变
F(i)= max(F(i-1)+array[i] , array[i])
res:所有子数组的和的最大值
res=max(res,F(i))

如数组[6, -3, -2, 7, -15, 1, 2, 2]
初始状态:F(0)= 6 res = 6

i=1:
F(1)=max(F(0)-3,-3)=max(6-3,3)=3
res=max(F(1),res)=max(3,6)=6

i=2:
F(2)=max(F(1)-2,-2)=max(3-2,-2)=1
res=max(F(2),res)=max(1,6)=6

i=3:
F(3)=max(F(2)+7,7)=max(1+7,7)=8
res=max(F(2),res)=max(8,6)=8

i=4:
F(4)=max(F(3)-15,-15)=max(8-15,-15)=-7
res=max(F(4),res)=max(-7,8)=8
以此类推
最终res的值为8

【初始DP】

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
    	//dp[i]表示以下标i为结尾的连续子数组最大和
    	//dp[i] = max(dp[i-1] + array[i], array[i])
    	//max_sum = max(dp[i])

    	vector<int> dp(array.size());
    	dp[0] = array[0];
    	int max_sum = array[0];
    	for(int i = 1; i < array.size(); i++){
    		dp[i] = max(dp[i-1] + array[i], array[i]);
    		if(dp[i] > max_sum)
    			max_sum = dp[i];
    	}
    	return max_sum;
    }
};

【优化内存DP】

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        int cur_sum = array[0];
        int max_sum = array[0];
        for(int i = 1; i < array.size(); i++){
            cur_sum = max(cur_sum + array[i], array[i]);
             
            if(cur_sum > max_sum)
                max_sum = cur_sum;
        }
        return max_sum;
    }
};

【返回数组】

void fun1(vector<int> vec){
    if(vec.empty())
        return;
        
    vector<int> len(vec.size(), 1);
    int max_val = vec[0];
    int cur_sum = vec[0];
    int max_end_idx = 0;
    
    for(int i = 1; i < vec.size(); i++){
        if(cur_sum <= 0){
            cur_sum = vec[i];
        }
        else{
            cur_sum += vec[i];
            len[i] = len[i-1] + 1;
        }
        
        
        if(cur_sum > max_val){
            max_val = cur_sum;
            max_end_idx = i;
        }
    }
    
    vector<int> res;
    for(int j = max_end_idx - len[max_end_idx] + 1; j <= max_end_idx; j++)
        res.push_back(vec[j]);
    output_vec(res);
}

6.5 最佳股票买卖时间

【链接】
https://www.nowcoder.com/questionTerminal/03905f7b819241398b02ee39bef3e8f1

【题目描述】
假设你有一个数组,其中第i个元素是某只股票在第i天的价格。
设计一个算法来求最大的利润。你最多可以进行两次交易。
注意:
你不能同时进行多个交易(即,你必须在再次购买之前出售之前买的股票)。

【解题思路】
整体思路,两个交易点,因为不能同时多个交易,可以拆分为两个子任务
从0到i,在i点【卖出】的最大收益,记为第一次交易
从j到len-1,在j点【买入】的最大收益,记为第二次交易
约束:i < j,遍历max(两者收益和)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //边界判断
        int len = prices.size();
        if(len <= 1)
            return 0;
        
        /*
        作用:i点【卖出】的最大收益 -> 输出:一个数组
        暴力写法:O(n^2),从前向后逐个判断
        O(n)复杂度思路:
        速度优化,子任务里面计算中间结果可复用
        当前点卖出收益 = 当前值 - 前面最小值,从前向后
        维护一个最小值
        */
        vector<int> vec_i(len, 0);
        int min_price = prices[0];
        for(int i = 1; i < len; i++){
            vec_i[i] = max(prices[i] - min_price, 0);
            
            //维护最小值
            if(prices[i] < min_price)
                min_price = prices[i];
        }
        
        /*
        作用:j点【买入】的最大收益计算 -> 输出:一个数组
        暴力写法:O(n^2),从前向后逐个判断
        O(n)复杂度思路:
        速度优化,子任务里面计算中间结果可复用
        当前点买入最大收益 = max(后面的最大值 - 当前值,0),从后向前
        维护一个最大值
        */
        
        vector<int> vec_j(len, 0); //记录每一个天买入后的最大利润,初始化为0,即当天买入卖出
        int max_price = prices[len-1];
        for(int j = len-2; j >= 0; j--){
            vec_j[j] = max(max_price - prices[j],0);
            
            //维护最大值
            if(prices[j] > max_price) 
                max_price = prices[j];
        }
        
        /*
        作用:输出最大利润,int值
        约束:最多两次交易,不能同时多个交易,即第一次交易结束点(卖出时间) <= 第二次交易开始点(买入时间)
        暴力解法:两层for循环
        特例:整条序列单调递增,一次交易得到最大利润
        可以更优嘛?:貌似没有
        维护一个最大利润值
        */
        int max_profit = 0; //不交易
        for(int i = 0; i < len; i++){
            if(vec_i[i] > max_profit) //一次交易
                max_profit = vec_i[i];
            for(int j = i+1; j < len; j++){
                if(vec_i[i] + vec_j[j] > max_profit) //二次交易
                    max_profit = vec_i[i] + vec_j[j];
            }
        }
        return max_profit;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值