非科班进大厂必备算法

2023-10-27

基础数据结构的融合是成为庞大系统的基石。比如 Redis 中的跳跃表,数据库索引 B+树等,只有对基础的数据结构足够的熟悉才能更容易去理解稍微复杂的结构,就仿佛我们闯关打怪一样,一步一步解锁直到结局。今天想和大家一起分享的是常见数据结构以及面试中的高频手撕算法题,一定要去手动写这些代码,可说百分之七八十都是这些题,一定要好好掌握。

1 数据结构

链表属于数据结构中的线性结构的一种,我们先看看什么是数据结构

  • 数据结构是:结构的定义+结构的操作

想必大伙儿应该玩儿过拼图,拼图之前我们先看看说明书,看看包含几个部分,然后对这些部分进行拼装,随后拼好候进行组合直到完成。

那么数据结构中的结构定义是这个数据结构长什么样子,有些什么性质?结构的操作意思是这个结构可以支持什么操作,但是不管你怎么的操作,不能破坏了它的结构

2 链表定义

一个链表是由 1 个或者多个节点组成,每个节点包含两个信息,一个是数据信息,用来存储数据,一个是地址信息,用来存储下个节点的地址

链表结构由一个个节点组成,我们不需要对结构做任何改变,只需要按照需求修改链表结构中的数据域即可。从上图我们知道此事数据域类型为整型 763,指针域为 0x56432,这个地址正好是第二个节点的地址,所以这两个节点在逻辑上是有个指向关系,也是通过这种方式将两个节点进行了关联。

第二个节点中的指针域为 0x0,这是一个特殊的地址,叫做空地址,指向空地址意味着它是这个链表结构的最后一个节点。

那在代码中是什么样子呢

struct Node {	int data;	struct Node *next;};

复制代码

这个结构很清晰,数据域根据我们的需求而定,想存整型就改成整型,想存字符串就写字符串。而指针域用来维护整个链表结构,一般来说直接用即可,如果需要内存中的链表结构,一定要修改节点内部 next 指针域中存储的地址值

3 链表操作

说到链表结构,我们习惯性的和数组联系在一起。只是数组结构在内存中是连续的,而链表结构因为指针域的存在,每个节点在内存中存储的位置未必连续。下面我们按照数组的方式给链表也编个号。

下面我们定义一个向链表插入节点的函数

```c++

struct Node insert(struct Node head, int ind, struct Node *a);

- 第一个参数为待操作的链表的头结点地址,也就是第一个节点的地址- 第二个参数为插入位置- 第三个参数为指针变量,指向要插入的新节点
简单的说就是向 head 指向的链表的 ind 位置插入一个由 a 指向的节点,返回值为插入新节点后的**表头地址**。为什么要返回它呢?因为我们插入的节点很可能在头部,此时就会改变链表的结构且改变头结点地址,所以需要返回。
那么我们插入一个元素,显然会改变链表的节点,操作方法为修改链表节点的 next 指针域即可,那么为了插入成功,我们需要修改哪些节点呢?
首先是让 ind - 1 位置的节点指向 a 节点,然后是 a 节点指向原 ind 位置的节点,也就是说,涉及到两个节点的 next 指针域的值的修改,一个是 ind - 1 位置的节点,一个是 a 节点自身。我们就可以先找到 ind - 1 位置的节点,然后再进行相关操作即可。
```c++struct Node *insert(struct Node *head, int ind, struct Node *a) {	struct Node ret, *p = &ret;	ret.next = head;	// 从虚拟头节点开始向后走 ind 步	while (ind--) p = p->next;	// 完成节点的插入操作	a->next = p->next;	p->next = a;	// 返回真正的链表头节点地址	return ret.next;}

复制代码

这里非常关心且非常重要的是虚拟节点。我们为什么引入虚拟节点?是为了让我们的插入操作统一化?什么是统一化?举个例子,假设我们现在是在第 5 个位置插入元素,我们自然需要从头遍历到第四个节点,确定了第四个节点后,修改相关的 next 指针域,也就是如果我们想插入到 nid 位,就需要从头节点向后移动 ind-1 步,那么如果插入的位置为 0 呢?我们总不能走-1 步吧,所以这个时候我们只好对 ind=0 的情况进行单独的判断了,这样明显是不完美了,所以我们为了统一 ind 在等于 0 和不等于 0 时的情况,引入虚拟节点。

ok,我们看看是不是方便了。增加了虚拟节点,如果插入第 5 个位置,我们只需要向后移动 5 位,如果插入到 0 号位置,向后移动 0 步即可,即 p 指针指向虚拟节点不懂,直接将新的节点插入到虚拟头结点后面完事儿。

好勒,这里出现了第一个重要的技巧。在我们插入链表节点的时候,加上虚拟节点是个实用技巧。

那么我们看看插入和删除的操作动态以及实现方式

4 案例

案例 1

我们看个题吧,定义一个快乐数,什么是快乐数,所谓快乐数即通过有限次变换后等于 1 的数字。怎么变换呢,给出一个非 1 的数字,然后出去位数,求各个位数的平方和,得到数字 A,假设 A 不死 1,那就继续对元素 A 的每一位进行平方和,得到数字 B。。。。知道最后能够=1

例如,一开始的数字是 19,经过变换规则 ,得到数字 82;因为不是 1 ,所以接着做变换,就是 ,再做一次变换 ,最后一次做变换,得到了 1 以后,停止

这个题的难点不是判断数是不是快乐数,而是如何判断一个数不是快乐数,如果不是快乐数,说明没有办法通过有限的次数到达数字 1,那么到底是 经过多少次呢?1k 次,10w 次?很难确定上限。在说这个问题之前我们先看几个高频链表练习题

例题 1 用数组判断链表中是否有环

在上面我们介绍了最后一个节点指向空,可是你有没有想过如果链表的最后一个节点不是空地址而是指向链表中的一个节点,这不就是环了?

如上图所示,节点 8 指向了 3,这样形成了 3,4,5,6,7,8 的环状结构,此时使用指针遍历链表将永无止境。那通过什么办法判断是否有环呢?

  • 使用数组标记的方法。记录出现过的节点信息,每次遍历新节点就去数组查看记录,这样的时间复杂度不给力。经过第一个节点,需要在数组查找 0 次,第 2 个节点,数组查找 1 次,第 i 个节点,在数组查找 i-1 次,直到遍历第 n+1 个节点,查找的总次数为(n + 1) * n / 2,这样时间复杂度为 O(n^2)。太慢了,给我优化

  • 快慢指针法

AB 两位同学跑步,A 同学速度快,B 同学速度慢,他们并不知道跑道是环形的,如果是环形,跑得快的,在足够的时间终究会从速度慢的 B 同学经过,形成相遇的情况。如果不是环形,速度快的先到重点,不会相遇---快慢指针法。

在这里,我们将链表当做跑道,跑道上两个指针,指针 A 每次走两步,指针 B 每次走两步,如果快的指针先跑到终点注定没有环,如果两指针相遇则有环。

```c++

int hasCycle(struct Node *head) {

if (head == NULL) return 0;

// p 是慢指针,q 是快指针

struct Node p = head, q = head;

// 每次循环,p 走 1 步,q 走 2 步

do {

p = p->next;

q = q->next;

if (q == NULL) return 0;

q = q->next;

} while (p != q && q);

return p == q;

}

## 3 二分查找初探
> 说到二分查找,这里就有个笑话了。
小孙同学去图书馆借书,一次性了借了40本书,出图书馆的时候报警了,不知道哪一本书没有**消磁**,然后把书放在地上,准备一本本尝试。
女生的操作被旁边的阿姨看见了,阿姨说你这样操作多慢啊,我来教你。于是将树分为两摞,拿出第一luo过一下安检,安检机器想了,于是阿姨将这摞书分成两部分,拿出一部分继续尝试,就这样,阿姨每次减少一半,没几次就找到了没有消磁的书。阿姨嘚瑟的来一句:小姑凉,这就是书中的二分查找算法,你这还得好好学习哇,第二天,图书馆发现丢了39本书。哈哈哈哈
## 4 二分查找基础
> 最简单的二分算法即在一个有序数组中,查找一个数字X是否存在。注意有序性。那么如何在数组中查找一个数
- 从头到尾一个一个查找,找到即有数字x- 二分算法即通过确定一个区间,然后查找区间的一半和x比较,如果比x大则在x前半段查找。如果比x小则在后半段查找,只需要log2n的比较即可确定结果。

![二分初探](https://img-blog.csdnimg.cn/20200906221123472.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0wxNTUxOTU0MzgzNw==,size_1,color_FFFFFF,t_70#pic_center)
图中呢,我们以查找 17 这个数字为例,L 和 R 所圈定的,就是当前的**查找区间**,一开始 L= 0,R = 6,mid 所指向的就是数组的中间位置,根据 L 和 R 计算得到 mid 的值是 3。查看数组第 3 位的值是 12,比待查找值 17 要小,说明如果 17 在这个有序数组中,那它一定在 mid 所指向位置的后面,而 mid 本身所指向的数字已经确定不是 17 了,所以下一次我们可以将查找区间,定位到 mid + 1 到 R,也就是将 L 调整到 mid + 1 (即数组第 4位)的位置。
**1 第一种小白写法**
```c++int BinarySerach(vector<int>& nums,int n, int target) {    int left = 0, right = n-1;    while (left <= right) {        int mid = (left+right)/2;        if (nums[mid] == target) return mid;        else if (nums[mid] < target) left = mid + 1;        else right = mid-1;    }    return -1;}

复制代码

面试官发话了

方法二优化版

如果 right 和 left 比较的时候,两者之和可能溢出。那么改进的方法是 mid=left+(right-left)/2.还可以继续优化,我们将除以 2 这种操作转换为位运算 mid=left+((right-left)>>1).

哪有这么简单的事儿,大多数的笔试面试中可能会出现下面的几种情况。

二分的各种变种

这里主要是看看原始数组有重复数的情况。

1 查找第一个值等于给定值的情况(查找元素 7)

思路

首先 7 与中间值 a[4]比较,发现小于 7,于是在 5 到 9 中继续查找,中间 a[7]=7,但是这个数 7 不是第一次出现的。那么我们检查这个值的前面是不是等于 7,如果等于 7,说明目前这个值不是第一次出现的 7,此时更新 rihgt=mid-1。ok 我们看看代码

```c++

int BinarySerach(vector<int>& nums, int n,int target) {

int left = 0, right = n-1;

while (left <= right) {

int mid = left+((right-left)>>1);

if (nums[mid]>value)

{

right=mid-1;

} else if(nums[mid]<value)

{

left=mid+1;

}else

{

if((mid==0)||(nums[mid-1]!=value))

{

return mid;

}else

{

left=mid-1;

}

}

return -1;

}

**2 查找最后一个值等于给定值的情况**
> 假设nums[mid]这个值已经是最后一个元素了,那么它肯定是要找到最后一个值。如果nums[mid]的下一个不等于value,那说明nums[mid]就是我们需要找到最后一个等于给定值的值。
```c++int BinarySerach(vector<int>& nums, int n,int target) {    int left = 0, right = n-1;    while (left <= right) {        int mid = left+((right-left)>>1);        if (nums[mid]>value)        {            right=mid-1;        } else if(nums[mid]<value)        {            left=mid+1;        }else        {            if((mid==n-1)||(nums[mid+1]!=value))            {                return mid;            }else            {                left=mid+1;            }        }    return -1;}

复制代码

3 查找第一个大于等于给定值的情况

  • 如果 nums[mid]小于要查找的值,那么我们需要查找在[mid+1,right]之间,所以此时更新为 left=mid+1

  • 如果 nums[mid]大于给定值 value,这个时候需要查看 nums[mid]是不是我们需要找的第一个值大于等于给定值元素,如果 nums[mid]前面没有元素或者前面一个元素小于查找的值,那么 nums[mid]就是我们需要查找的值。相反

  • 如果 nums[mid-1]也是大于等于查找的值,那么说明查找的元素在[left,mid-1]之间,所以我们需要将 right 更新为 mid-1

```c++

int BinarySerach(vector<int>& nums, int n,int target) {

int left = 0, right = n-1;

while (left <= right) {

int mid = left+((right-left)>>1);

if (nums[mid]>value)

{

right=mid-1;

} else if(nums[mid]<value)

{

left=mid+1;

}else

{

if((mid==n-1)||(nums[mid+1]!=value))

{

return mid;

}else

{

left=mid+1;

}

}

return -1;

}

**4 查找第一个大于等于给定值的情况**
- 如果nums[mid]小于要查找的值,那么我们需要查找在[mid+1,right]之间,所以此时更新为left=mid+1- 如果nums[mid]大于给定值value,这个时候需要查看nums[mid]是不是我们需要找的第一个值大于等于给定值元素,如果nums[mid]前面没有元素或者前面一个元素小于查找的值,那么nums[mid]就是我们需要查找的值。相反- 如果nums[mid-1]也是大于等于查找的值,那么说明查找的元素在[left,mid-1]之间,所以我们需要将right更新为mid-1
```c++int BinarySerach(vector<int>& nums, int n,int target) {    int left = 0, right = n-1;    while (left <= right) {        int mid = left+((right-left)>>1);        if (nums[mid]>=value)        {            if(mid==0||nums[mid-1]<value)            {                return mid;            }else            {                right=mid-1;            }        }else        {            left=mid+1;        }    return -1;}


复制代码

5 查找最后一个小于等于给定值的情况

  • 如果 nums[mid]小于查找的值,那么需要查找的值肯定在[mid+1,right]之间,所以我们需要更新 left=mid+1

  • 如果 nums[mid]大于等于给定的 value,检查 nums[mid]是不是我们的第一个值大于等于给定值的元素

```c++

int BinarySerach(vector<int>& nums, int n,int target) {

int left = 0, right = n-1;

while (left <= right) {

int mid = left+((right-left)>>1);

if (nums[mid]>value)

{

right=mid-1;

}else

{

if(mid==n-1||(nums[mid+1]>value))

{

return mid;

}else

{

left=mid+1;

}

}

return -1;

}

## 4 队列
> 例子:滑动窗口最大值
**队列回忆:**
> 火车站买票应该都经历过,窗口小姐姐每次服务排在最前面的那个人,买完票则从头部离开,后面人往前一步接替离开的人继续购票,这就是典型的队列结构。
计算机中的队列和其类似,先到先得,先入先出,每个元素从尾部入队,从头部处理完出队
![队列定义](https://img-blog.csdnimg.cn/20200906220007386.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0wxNTUxOTU0MzgzNw==,size_1,color_FFFFFF,t_70#pic_center)
**单调队列**
> 假设将学生从高年级到低年级排列,随着时间的推移,高年级同学从队列头部毕业,低年级从尾部进入。大部分学校都有校队,假设小林高三,我高二,小七高一,小林毕业接班的是我,我毕业,很可能就是小七接班,而当我进队的那一刻,小七即使进的早但是战斗力没我高,所以小七是永远没计划被选中啦。所以,纵观全队,不仅有着队列的性质,也有着单调的性质,所以就是单调队列。
**为什么需要单调队列**
> 比较明显的作用是,用来维护队列处理顺序中的区间最大值。
**高频面试题----滑动窗口最大值**
> 滑动窗口没向后滑动一位,就有一个元素从队首出队,同时也会有个元素从队尾入队。这个题需要求区间的最大值:意味着需要维护在队列处理顺序中的区间最大值,直接上代码附上注释
```c#define MAX_N 1000int q[MAX_N + 5], head, tail;void interval_max_number(int *a, int n, int m) {	head = tail = 0;	for (int i = 0; i < n; i++) {        // a[i] 入队,将违反单调性的从队列 q 中踢出        while (head < tail && a[q[tail - 1]] < a[i]) tail--;        q[tail++] = i; // i 入队        // 判断队列头部元素是否出了窗口范围        if (i - m == q[head]) head++;        // 输出区间内最大值        if (i + 1 >= m) {            printf("interval(%d, %d)", i - m + 1, i);            printf(" = %d\n", a[q[head]]);        }	}		return ;}

复制代码

5 栈与单调栈

栈结构对应于队列,可以将栈想象为一个只有单出口的羽毛球筒,羽毛球只能从单一的入口放入和取出。假设我们将 1,2,3 三个球放进球桶,如果取出来此时就是 3,2,1。性质就很明显了,先进后出的结构

栈结构本身维护的是一种完全包含的关系。这种包含关系在函数之间的运行体现的玲离尽致,也就是一种包含关系,如果主函数调用函数 B,那么函数 B 一定会在主函数结束之前结束。

单调栈

此时应该了解了栈和队列,那么我问你,你觉得栈和队列最大的区别是啥?

你可能毫不犹豫的可以回答栈是先进后出,队列是先进先出。ok,那我再问你,堵住了出口的单调队列和栈有什么区别?这是不是就没什么区别了,单调队列为了维护其单调性,在入队的时候会将违反单调性的元素弹出去,这就相当于栈的同一段进出,是的,堵住出口的单调队列就是我们现在要说的单调栈,目前以单调递减栈为例

当序列中的 12 号元素入栈以后,此时单调栈有 4 个元素,从栈底到栈顶分别为 23,18,15,9,按照原始序列为 2 5 9 12。此时我们关注 12 号元素和 9 号元素的关系。如果 12 号元素入栈,为了保证栈的单调递减性,最终放在 9 号上面,此时我们虽然不是第十个元素和十一号元素值多少,但是这两个元素的值一定是比 9 号元素小,这就是单调栈的性质。所以,单调队列是用来维护区最值的高效结构,单调栈呢是维护最近大于或小于的高效结构。下面看个例子

题目:判断括号序列是否合法

示例 合法

({})

[]([]){()}

示例 非合法

([)]

(((){}

public boolean isValid(String s) {        Stack<Character> stack = new Stack<>();        Map<Character,Character> map = new HashMap<>();        char[] chars = s.toCharArray();        map.put(')','(');        map.put('}','{');        map.put(']','[');        for(int i=0;i < s.length();i++){            if(!map.containsKey(chars[i])) {            	//为左括号时直接入栈                stack.push(chars[i]);            }else{            	//为右括号时,如果栈为空或者栈顶与该括号类型不匹配返回false                if(stack.empty() || map.get(chars[i]) != stack.pop()){                    return false;                }            }        }        //字符串遍历完毕后,如果栈为空返回true,反之返回false        return stack.empty();    }

复制代码

6 递推套路

在分享递推之前,先和大家分享与之紧密的数学原理:容斥原理

在计数问题中,为了保证计数的准确程度,通常会保证两个问题,第一个问题是没有重复,第二个问题是没有遗漏。这两个问题相对来说,第二点比较容易做到。比如对某地区进行爆炸式轰炸,为了保证炸的覆盖面,足够多的炸弹即可,但是如果保障一块土地只能炸一次就比较难搞了。那么容斥原理就是解决这个问题

容斥原理是什么?

先不考虑重叠的情况,先将所有对象数目计算出来,然后将重复计算的排斥出去,是的,计算的结果不仅不遗漏也不重复。简单的说就是在计算的过程中,如果加多了就减去多的部分,如果减多了就加回来一部分,直到不多不少。

我们看一个兔子繁殖问题

假设有一片草原上,莫名其妙来了一只外星兔子,这种外星兔子呢,第一个月的时候是幼体,第二个月成长为成体,从第三个月开始,成体兔子每个月都会产生出一只克隆体的幼体兔子,而且这种兔子不会衰老,一旦成体以后,就会一直生下去。按照这种情况,请你计算出第 n 个月,草原上有多

少只兔子?

此时给出前面 6 个月的情况

从上图我们可以发现,从第一个月到第六个月,草原上的兔子数量分别为 1,1,2,3,5,8

第六个月共有 8 只兔子,其中包含 5 只成兔,3 只幼兔,为什么是 5 只成兔,因为第六个月的兔子数量等于第五个月的兔子总数,六个月的 3 只幼兔是等于第四个月的兔子数量

结论就比较清晰了:从第三个月开始,第 n 个月的兔子数量等于该月的成兔数量与幼兔数量之和,也就是等于第 n-1 个月的兔子数量与第 n-2 兔子数量之和。这种根据前面的数量来推后面的数量的情况叫做递推,那么递推算法套路通常是怎么样呢

  • 确定递推的状态,多画图前面几步

  • 推导递推公式

  • 程序的编写

我们根据三步走的方式来阐释解决兔子的这个问题

  • f(n)表示 n 个月兔子的数量

  • 递推公式(第一个月合第二个月兔子的数量为 1,到了第三个月即等于前面两个月之和)

案例 2 凑钱币问题

用 1 元、2 元、5 元、10 元、20 元、50 元和 100

元凑成 1000 元钱,总共有多少种方案

  • 确定递推状态,需要分析自变量与因变量,自变量两个分别为币种种类和拼凑的钱币数量,因变量 1 个为方案总数,因此我们的状态定义为 f(i,j),i 种钱币,拼凑 j 元钱的方案总数。比如 f \[3][10]即使用三种钱币,凑出 10 元的方案总数

  • 假设我们不使用第三种钱币,那么此时等价于使用前两种钱币拼凑 10 元钱的方案总数,即 f\[2][10]。如果使用至少 1 张 5 块钱,那么我们在这些方案中去掉一张 5 元钱,剩下的方案数为 f\[3][5],所以此时的递推公式为 f\[3][10] = f\[2][10] + f\[3][5]。这只是一般情况,假设我们没有使用第 i 种钱币,拼凑 j 元的方案为 f(i-1,j),代表使用前 i-1 种钱币的方案总数。剩下的使用了第 i 中钱币,由于都存在第 i 钱币 1 张,假设第 i 种钱币的面额为 val[i],那么此时我们的前 i 种钱币,凑 j-val[i]的钱数,此时方案总数为 f(i,j-val[i]);所以公式为 f(i,j)=f(i-1,j)+f(i,j-val[i])

7 动态规划

动态规划通常简称 DP(dynamic programming),如果按照问题类型来划分,将分为线性 DP、区间 DP,数位 DP 等等,每当说到动态规划就会想最优子结构,重叠子问题等等,这些词汇苦涩难懂,不要慌,再难的问题也是建立在基础问题上,逐步拆分,这也是动态规划的思想,相信通过下面动态规划四步走的方式,加上习题的练习,一定会让你对动态规划有个新的理解。

四个步骤分为:状态定义,状态转移方程,正确性的证明和实现

  • 状态定义

其实上面说递推的时候就已经有所涉及状态定义,通常在推导的过程中,如果感觉推不下去了,很有可能就是我们的状态定义出现了问题。

第一个状态:dp\[i][j]代表从起始点到(I,j)路径的最大值

第二个状态:dp\[i][j]代表从底边的某个点出发,到达(i,j)路径的最大值

  • 状态转移方程

上面的两种状态定义对应这里两个转移方向。

如上图所示,我们想要求得 dp\[i][j],需要知道 dp|[i-1]|[j-1]和 dp\[i-1][j]的值。因为只有(i - 1, j - 1) 和 (i - 1, j) 这两个点,才能能走到 (i, j)此时的状态转移方程为

第一种状态转移方程 dp\[i][j] = max(dp\[i - 1][j - 1], dp\[i - 1][j]) + val\[i][j]

第二册中状态转移方程 dp\[i][j] = max(dp\[i + 1][j], dp\[i + 1][j + 1]) + val\[i][j]

从这里可以知道我们的状态定义不一样,我们的转移方程就很不一样吧

  • 正确性证明

数学归纳法通常采用三步走的方式,常用的正确性证明方法为数学归纳法。

第一步,第一个阶段所有 dp 值可以轻松获得,也就是初始化 dp\[1][1],等于 val\[1][1]

第二步,假设如果第 i-1 阶段的所有状态值都正确得到,那么根据状态方程 dp\[i][j]=max(dp\[i - 1][j], dp\[i - 1][j + 1]) + val[i][j] 来说,此时就可以计算得到第 i 阶段中的素有状态值

第三步:得出结论,所有的状态值计算正确

我们继续分析动态规划问题中的 0/1 背包问题,通常分为三类,0/1 背包问题,完全背包问题和多重背包问题。

0/1 背包问题是另外两种背包问题的基础,简单描述一下,假设有个背包,载重上限为 W,此时有 n 个物品,第 i 个物品的重量是 wi,价值为 vi,那么在不超过背包重量上限的前提下,能获得的最大物品价值总和?同样我们采用四步走的方式

  • 状态定义

首先分析背包问题中的自变量和因变量,其中因变量比较好确定,就是所求最大价值总和,自变量呢,在此自变量为物品种类和背包承重上限,因为这两者会影响价值总和的最大值,所以我们设置一个二维状态。dp\[i][j]代表使用前 i 个物品,背包最大载重为 j 的情况下最大价值总和。

  • 状态方程

说白了就是找映射函数,dp\[i][j]的表达式。我们将 dp\[i][j]分为两大类,第一类是不选择第 i 个物品最大价值和,第二类为选择了第 i 个物品的最大价值和。然后在两者中选择最大值就是价值更大的方案。

如果选择第 i 个物品,此时的最大价值为 dp\[i-1][j-wi]+vi,既然选择了第 i 个商品,那么就需要留出一个位置,那么此时对于剩余的 i-1 个商品的载重空间就只剩下 j-wi 了,此时 i-1 个物品选择的最大价值和为 dp\[i-1][j-wi],然后加上 vi 就是当前获得最大价值和。所以转移方程为

dp[i][j] = max(dp\[i - 1][j], dp\[i - 1][j - w[i]] + v[i])

  • 正确性证明

首先 dp\[0][j]=0,意味着没有物品的时候,无论背包限重多少,能够得到的最大价值和都是 0,所以 k0 争取

其次,假设我们已经获取 i-1 个物品的价值最大值,所有 dp\[i-1]的值,那么根据状态方程,我们能知道所有 dp\[i]的值

最后两步联合,整个求解对于任意 dp\[i][j]成立

  • 实现

#define MAX_V 10000#define MAX_N 100int v[MAXN + 5], w[MAXN + 5];int dp[MAXN + 5][MAXV + 5];int get_dp(int n, int W) {    // 初始化 dp[0] 阶段    for (int i = 0; i <= W; i++) dp[0][i] = 0;    // 假设 dp[i - 1] 成立,计算得到 dp[i]    // 状态转移过程,i 代表物品,j 代表背包限重    for (int i = 1; i <= n; i++) {   	 	for (int j = 0; j <= W; j++) {        // 不选择第 i 种物品时的最大值        dp[i][j] = dp[i - 1][j];        // 与选择第 i 种物品的最大值作比较,并更新        if (j >= w[i] && dp[i][j] < dp[i - 1][j - w[i]] + v[i]) {        	dp[i][j] = dp[i - 1][j - w[i]] + v[i];   		 }   	 }	}	return dp[n][W];}

复制代码

8 贪心

其实我们大学学习的好几种应用都是采用了贪心的算法啊,比如 Huffman Coding,Prim 最小生成树等

先来看一道之前美团的一道笔试题--跳一跳

有 n 个盒子排成一行,每个盒子上面有一个数字 a[i],表示最多能向右跳 a[i]个盒子;小林站在左边第一个盒子,请问能否到达最右边的盒子?比如说:[1, 2, 3, 0, 4] 可以到达第 5 个盒子;[3, 2, 1, 0, 4] 无法到达第 5 个盒子;

思路:自然而然的想法,尽可能的往右边跳,看最后能够到达,从第一个盒子开始从右遍历,对于每个经过的盒子,不断地更新 maxRight 值。那么贪心算法的思考过程通常是怎么样的?

类似于动态规划,大事化小,小事化了。所谓大事化小,将大的问题,找到和子问题的重复部分,将复杂问题拆分为小的问题。小事化了,通过对小事的打磨找到较为核心的策略。上例子

分糖果问题

我们有 m 个糖果和 n 个孩子。我们现在要把糖果分给这些孩子吃,但是糖果少,孩子多(m<n),所以糖果只能分配给一部分孩子。 每个糖果的大小不等,这 m 个糖果的大小分别是 s1,s2,s3,……,sm。除此之外,每个孩子对糖果大小的需求也是不一样的,只有糖果的大小大于等于孩子的对糖果大小的需求的时候,孩子才得到满足。假设这 n 个孩子对糖果大小的需求分别是 g1,g2,g3,……,gn。

我的问题是,如何分配糖果,能尽可能满足最多数量的孩子?

  • 对于一个孩子来说,如果小的糖果可以满足,那么就没必要用更大的糖果

  • 对糖果的大小需求的孩子更容易被满足,所以我们可以从需求小得孩子开始分配糖果

  • 因为满足一个需求大的孩子跟满足一个需求小的孩子,对我们期望值的贡献是一样的

  • 我们每次从剩下的孩子中,找出对糖果大小需求最小的,然后发给他剩下的糖果中能满足他的最小的糖果,这样得到的分配方案,也就是满足的孩子个数最多的方案

#include <iostream>#include <vector>#include <algorithm>using namespace std;/*解题思路:遍历两边,首先每个人得一块糖,第一遍从左到右,若当前点比前一个点高就比前者多一块。这样保证了在一个方向上满足了要求。第二遍从右往左,若左右两点,左侧高于右侧,但左侧的糖果数不多于右侧,则左侧糖果数等于右侧糖果数+1,这就保证了另一个方向上满足要求。最后将各个位置的糖果数累加起来就可以了。*/int candyCount(vector<int>&rating) {    int res = 0;    //孩子总数    int n = rating.size();    //糖果集合    vector<int> candy(n, 1);    //从左往右遍历    for (int i = 0;i < n - 1;i++) {        if (rating[i + 1] > rating[i])candy[i + 1] = candy[i] + 1;    }    //从右往左    for (int i = n - 1;i > 0;i--) {        if (rating[i - 1] > rating[i] && candy[i - 1] <= candy[i])        	candy[i - 1] = candy[i] + 1;    }    //累加结果    for (auto a : candy) {        res += a;    }    return res;}//测试函数int main() {    vector<int> rating{1,3,2,1,4,5,2};    cout << candyCount(rating) << endl;    return 0;}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

非科班进大厂必备算法 的相关文章

  • 测试工程师都是怎么写测试用例的?浅谈测试用例的编写思路和执行

    性能测试 压力测试 负载测试 强度测试 稳定性测试 健壮性测试 功能测试 系统测试 集成测试 接口测试 这么些眼花缭乱的测试类型名称 估计很少有有人能准确地区分和说出定义来 对应的测试用例如何编写和执行 就更不容易进行了 如果问测试工程师测
  • 冒泡排序/选择排序/插入排序/快速排序/归并排序/桶排序/堆排序/希尔排序/计数排序/基数排序/二分查找/广度优先搜索/深度优先搜索

    排序算法 冒泡排序 Bubble Sort 通过重复地比较相邻的元素并交换它们 使得最大 或最小 的元素逐渐移动到列表的一端 从而实现排序 选择排序 Selection Sort 在未排序的部分中 选择最小 或最大 的元素 并将其放置在已排
  • 164页,2023新版《Java面试手册》,抓住机会向前冲

    小伙伴们 2023新版 Java面试手册 来啦 这本小册子总计164页 全都是面试中的高频题目 有兴趣的小伙伴们不妨来看一下 为上岸做一下准备 由于全部内容过多 下面截取部分内容截图 大家可以先来大体看一下 Java基础 由于平台文章篇幅限
  • 【每日一题】2397. 被列覆盖的最多行数-2024.1.4

    题目 2397 被列覆盖的最多行数 给你一个下标从 0 开始 大小为 m x n 的二进制矩阵 matrix 另给你一个整数 numSelect 表示你必须从 matrix 中选择的 不同 列的数量 如果一行中所有的 1 都被你选中的列所覆
  • 系统有万亿条消息怎么存储?

    系统有万亿条消息怎么存储 本文转自 公众号 ByteByteGo 如有侵权 请联系 立即删除 我们如何设计一个能存储数万亿条信息的系统 Discord 的消息存储演进给我们提供了真实案例参考 下图显示了 Discord 消息存储的演变过程
  • 【数据结构和算法】小行星碰撞

    其他系列文章导航 Java基础合集 数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一 题目描述 二 题解 2 1 什么情况会用到栈 2 2 方法一 模拟 栈 三 代码 3 1
  • 浅谈归并排序:合并 K 个升序链表的归并解法

    在面试中遇到了这道题 如何实现多个升序链表的合并 这是 LeetCode 上的一道原题 题目具体如下 用归并实现合并 K 个升序链表 LeetCode 23 合并K个升序链表 给你一个链表数组 每个链表都已经按升序排列 请你将所有链表合并到
  • 史上最全的中高级 JAVA 工程师面试题汇总有哪些?

    你有面试机会了吗 近期 肯定有很多小伙伴 投出去的简历HR基本上都是已读不回 甚至都没有任何回复 或者平台默认筛选 你的简历HR根本就看不到 即使有些小伙伴简历通过 收到面试邀请了 结果被通知不用面试了 还有些小伙伴 有面试机会了 甚至已经
  • 2024 年最新版 Java 面试题及答案整理(纯干货,超详细)

    程序员一步入中年 不知不觉便会被铺天盖地的 危机感 上身 曾经的那个少年已经不在 时间就是这样公平 就算你能发明 Java 语言 随着时间的推移 你注定还是要成为慢慢变蔫的茄子 缓缓变黑的葡萄 看着春招就要来临的消息 吓得我周末赶紧拿出了面
  • 2024年最热门的15个科技工作岗位

    1 系统安全管理员 系统安全管理员的任务是确保公司的网络 数据和系统免受网络安全威胁 方法是确保有适当的安全战略并保持最新的合规性和策略 要求 应聘者应具有网络安全职位的工作经验 并对合规性和安全协议的最佳实践有坚实的基础 这个职位通常需要
  • 排序:计数排序

    一 概念 计数排序是非比较排序 是对哈希直接定址法的变形应用 二 思想 利用数组统计相同数据出现的次数 例如整型数据m出现n次 就在数组m位置记录数据为n 最后从头遍历数组打印数据即可 通俗来讲就是 数组下标即为数据 下标所指位置的值即为数
  • 206.翻转链表

    翻转链表 力扣 LeetCode 官网 全球极客挚爱的技术成长平台 备战技术面试 力扣提供海量技术面试资源 帮助你高效提升编程技能 轻松拿下世界 IT 名企 Dream Offer https leetcode cn problems re
  • 基于Loadrunner的性能分析及调优经验分享

    公司某个系统的微信端计划将开放给几百上千的人员登录查询 并且登录账号为同一账号多人使用 后台服务能够支撑起多用户的并发操作以及成百上千人登录微信端对生产数据库或者登录查询的性能效率高成为交付可靠生产环境的必要条件 因此 项目组决定提交测试
  • 单向不带头链表的使用

    单向不带头链表的使用 链表的创建 typedef struct LNode SLDataType data struct LNode next LNode LinkList 按位查找 LNode GetElem LinkList L int
  • Android Navigation的四大要点你都知道吗?

    在JetPack中有一个组件是Navigation 顾名思义它是一个页面导航组件 相对于其他的第三方导航 不同的是它是专门为Fragment的页面管理所设计的 它对于单个Activity的App来说非常有用 因为以一个Activity为架构
  • 程序员找工作难!拿到外包公司的 offer 我应该去么?

    引言 前一阵子有一个帖子引起了非常广泛的讨论 描述的就是一个公司的外包工作人员 加班的时候因为吃了公司给员工准备的零食 被公司的HR当场批评 这个帖子一发出来 让现在测试行业日益新增的外包公司备受关注 那么外包公司和非外包公司有什么样的不一
  • Synchronized 锁机制

    为了避免临界区的竞态条件发生 可以用非阻塞式的原子变量 也可以用阻塞式的锁 Java 多线程的锁都是 对象锁 采用互斥的方式让同一时刻只有一个线程能够持有对象锁 从而进入临界区 而其它线程只能阻塞等待 因此不用担心线程上下文切换造成共享资源
  • 2024史上最全Java面试八股文(带全部答案)

    今天要谈的主题是关于求职 求职是在每个技术人员的生涯中都要经历多次 对于我们大部分人而言 在进入自己心仪的公司之前少不了准备工作 有一份全面细致 面试题 将帮助我们减少许多麻烦 在跳槽季来临之前 特地做这个系列的文章 一方面帮助自己巩固下基
  • 面试官:分库分表后如何生成全局ID?

    分库分表后就不能使用自增 ID 来作为表的主键了 因为数据库自增 ID 只适用于单机环境 但如果是分布式环境 是将数据库进行分库 分表或数据库分片等操作时 那么数据库自增 ID 就会生成重复 ID 从而导致业务查询上的问题 所以此时 可以使
  • 深入解析 YAML 配置文件:从语法到最佳实践

    一 认识YAML YAML YAML Ain t Markup Language 是一种人类可读的数据序列化语言 它的设计目标是使数据在不同编程语言之间交换和共享变得简单 YAML采用了一种简洁 直观的语法 以易于阅读和编写的方式表示数据结

随机推荐

  • C++ 容器vector 语法练习

    编程不是什么技术活 就是个手工活 常常练习 否则手很生 前面写个一次 很久不用就忘记了 http blog csdn net sergery article details 8144354 cpp view plain copy C Pri
  • prometheus(二)——数据模型、数据模块、表达式浏览器

    文章目录 一 prometheus数据模型 1 概述 2 指标类型 3 作业job和实例targets instance 4 prometheusQL 数据查询语言也是时序数据库使用语言 二 prometheus数据模块 三 表达式浏览器
  • [Pytorch系列-49]:卷积神经网络 - 迁移学习的统一处理流程与软件架构 - Pytorch代码实现

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 https blog csdn net HiWangWenBing article details 121312731 目录 第1章 关于F
  • pythone二级题库 + 刷题软件 (超详细解析,看完必过) 第十套

    刷 题软件 模拟python二级考试 操作题刷题软件 1 某系统结构图如下图所示 该系统结构图的深度是 img src 10 1 3 A 4 B 2 C 3 D 1 本题考查知识点是深度 结构图的深度表示控制的层数 同一层上所有结点的所有子
  • 正则表达式之(年龄)(电话号码)(姓名)

    最近在项目中遇到验证的问题 年龄在1 180 姓名姓名为2 18中文和英文结合的字符 电话号码的验证 所以这里来备份一下 电话号码 let telphone 13 0 9 14 579 15 0 3 5 9 16 6 17 0135678
  • spss正态性检验_参数检验与非参数检验如何用?

    1 参数检验 两个独立样本 什么是独立样本 将研究对象随机的分配到两组中 分别接受不同的处理 或分别从两个总体中完全随机的抽取一部分个性进行研究 这样的样本就是独立样本 相应的T检验就是独立样本T检验 什么时候运用参数检验 什么时候运用非参
  • /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/libmysqlclient.a(libmysql.c.o): In function

    在CentOS系统中编译mysql程序 出现这种错误 usr lib gcc x86 64 redhat linux 4 8 5 lib64 libmysqlclient a libmysql c o In function fetch f
  • canvas--标尺

    项目开发过程中 会遇到标尺的功能 标尺可以通过canvas来实现 具体实现如下 1 html代码
  • Stata数据处理。如何处理删除某个公司的某个年份缺失的公司的数据

    stata处理数据 删除某个公司的某个年份缺失的公司的数据 例如 code year 1 2002 1 2003 1 2004 1 2005 2 2002 2 2003 2 2004 code有很多 不知道哪个年份缺少 如何解决 谢谢大家
  • selenium实现截图(python)

    由于需要使用python的selenium库来实现批量对URL的截图 在使用过程中遇到一些小坑 记录下来 使用selenium库截图 第一步 确定我们是需要使用selenium库的截图函数 即save screenshot test png
  • 论文阅读: Going Deeper with Point Networks

    论文题目 Going Deeper with Point Networks Arxiv 2019 pytorch 特点 减小显存使用 使得网络可以更深 效果变好的同时计算量增加不大 pointnet整个网络只有到最后才集合了全局特征 因此他
  • 抱薪者说

    Conflux 网络上线一周年在即 在这将近一年的时间里 Conflux 链上合约部署超 5 266 个 用户数超 476 381 共 24 129 394 笔交易被处理 链上集合了 400 开发者 共书写代码超 291 558 行 网络代
  • 在 Python 中如何实现类的继承,方法重载及重写?

    作者 苏凉 py 来源 CSDN博客 今天我们将进入类的继承以及对类的方法重写及重载的学习 话不多说直接进入正题 类的继承 如果要编写的类是另一个现成类的特殊版本 那我们就可以使用继承 一个类继承另一个类时 将自动获得另一个类的所有属性和方
  • 使用 FFmpeg 生成 ts 切片并使用 AES-128 加密

    前言 最近有个需求 需要将服务器视频资源进行加密提供给客户端播放 防止用户盗用视频 常用的加密方式 m3u8切片加密 本文使用 各种在线播放视频的网站广泛使用的技术 切片同样是使用AES加密算法 优点 各种浏览器 手机 小程序都能兼容 通用
  • CommonJS、AMD、CMD、ES Module

    依赖前置和依赖就近 RequireJS采用依赖前置 举个例子就是炒菜前一次性把所有食材洗好 切好 根据内部逻辑把有些酱料拌好 最后开始炒菜 前面任何一个步骤出现问题都能较早发现错误 SeaJS的依赖就近就是要炒青椒了去切青椒要炒肉了去切肉
  • docker容器资源控制cgroup

    命名空间 六种 namespace 资源配额 cgroups mount t cgroup cd sys fs cgroup cd memory 默认是没有限制 现在更改内存使用 free m mount t cgroup bc 1024
  • 金额转化中文算法

    最近要项目中用到了把数字类型的金额 1029 89元 转换成中文书写的方式 一仟零贰拾玖点八九元 参考了一些其他人写的算法 总觉得有些不太完善或者不严谨 例如10100转换成 十万一千元 还是 十万零一千元 我看到的一些算法都是转换成了前者
  • 解放生产力,社媒运营人还能这样玩转ChatGPT?

    相信大家这段时间都被ChatGPT刷屏了吧 东哥我也不例外 基本上一打开社媒平台都是在讨论ChatGPT 那社媒运营人应该如何使用ChatGPT呢 东哥今天就跟大家唠唠 利用ChatGPT写广告标语 广告文案 运营人常常为广告标语 广告文案
  • 一碗云南米线,加剧速食食品赛道“内卷”?

    说起云南 人们的印象往往是藏在苍山洱海 玉龙雪山里的风花雪月 然而 生活中最常见的 滇味 却是一碗鲜香美味 软中带劲的米线 近年来 从广西的螺蛳粉到河南的酸辣粉 越来越多带着地域特色的主食被装进小小纸桶 成为速食食品行业的新品类 如今 云南
  • 非科班进大厂必备算法

    基础数据结构的融合是成为庞大系统的基石 比如 Redis 中的跳跃表 数据库索引 B 树等 只有对基础的数据结构足够的熟悉才能更容易去理解稍微复杂的结构 就仿佛我们闯关打怪一样 一步一步解锁直到结局 今天想和大家一起分享的是常见数据结构以及