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
- 从后往前找到不满足递增排序的点
- 如果不存在这样的点证明数组是非递增的,直接reverse,然后返回即可
- 如果找到,例如上例中的7,则重新从后往前寻找大于3的第一个数,即5
- 交换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;
}
};