0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

常见数据结构以及面试中的高频手撕算法题

算法与数据结构 来源:算法与数据结构 作者:算法与数据结构 2020-10-30 09:56 次阅读

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

高频手撕算法合集

1、数据结构

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

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

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

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

2、链表定义

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

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

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

那在代码中是什么样子呢

structNode{ intdata; structNode*next; };

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

3、链表操作

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

单链表

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

structNode*insert(structNode*head,intind,structNode*a);

第一个参数为待操作的链表的头结点地址,也就是第一个节点的地址

第二个参数为插入位置

第三个参数为指针变量,指向要插入的新节点

简单的说就是向 head 指向的链表的 ind 位置插入一个由 a 指向的节点,返回值为插入新节点后的表头地址。为什么要返回它呢?因为我们插入的节点很可能在头部,此时就会改变链表的结构且改变头结点地址,所以需要返回。

那么我们插入一个元素,显然会改变链表的节点,操作方法为修改链表节点的 next 指针域即可,那么为了插入成功,我们需要修改哪些节点呢?

首先是让 ind - 1 位置的节点指向 a 节点,然后是 a 节点指向原 ind 位置的节点,也就是说,涉及到两个节点的 next 指针域的值的修改,一个是 ind - 1 位置的节点,一个是 a 节点自身。我们就可以先找到 ind - 1 位置的节点,然后再进行相关操作即可。

structNode*insert(structNode*head,intind,structNode*a){ structNoderet,*p=&ret; ret.next=head; //从虚拟头节点开始向后走ind步 while(ind--)p=p->next; //完成节点的插入操作 a->next=p->next; p->next=a; //返回真正的链表头节点地址 returnret.next; }

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

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

虚拟节点

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

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

3、案例

案例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每次走两步,如果快的指针先跑到终点注定没有环,如果两指针相遇则有环。

inthasCycle(structNode*head){ if(head==NULL)return0; //p是慢指针,q是快指针 structNode*p=head,*q=head; //每次循环,p走1步,q走2步 do{ p=p->next; q=q->next; if(q==NULL)return0; q=q->next; }while(p!=q&&q); returnp==q; }

3、二分查找初探

说到二分查找,这里就有个笑话了。

小孙同学去图书馆借书,一次性了借了40本书,出图书馆的时候报警了,不知道哪一本书没有消磁,然后把书放在地上,准备一本本尝试。

女生的操作被旁边的阿姨看见了,阿姨说你这样操作多慢啊,我来教你。于是将树分为两摞,拿出第一luo过一下安检,安检机器想了,于是阿姨将这摞书分成两部分,拿出一部分继续尝试,就这样,阿姨每次减少一半,没几次就找到了没有消磁的书。阿姨嘚瑟的来一句:小姑凉,这就是书中的二分查找算法,你这还得好好学习哇,第二天,图书馆发现丢了39本书。哈哈哈哈

4、二分查找基础

最简单的二分算法即在一个有序数组中,查找一个数字X是否存在。注意有序性。那么如何在数组中查找一个数

从头到尾一个一个查找,找到即有数字x

二分算法即通过确定一个区间,然后查找区间的一半和x比较,如果比x大则在x前半段查找。如果比x小则在后半段查找,只需要log2n的比较即可确定结果。

二分初探

图中呢,我们以查找 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 第一种小白写法

intBinarySerach(vector&nums,intn,inttarget){ intleft=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我们看看代码

intBinarySerach(vector&nums,intn,inttarget){ intleft=0,right=n-1; while(left<= right) {         int mid = left+((right-left)>>1); if(nums[mid]>value) { right=mid-1; }elseif(nums[mid]

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

假设nums[mid]这个值已经是最后一个元素了,那么它肯定是要找到最后一个值。如果nums[mid]的下一个不等于value,那说明nums[mid]就是我们需要找到最后一个等于给定值的值。

intBinarySerach(vector&nums,intn,inttarget){ intleft=0,right=n-1; while(left<= right) {         int mid = left+((right-left)>>1); if(nums[mid]>value) { right=mid-1; }elseif(nums[mid]

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

intBinarySerach(vector&nums,intn,inttarget){ intleft=0,right=n-1; while(left<= right) {         int mid = left+((right-left)>>1); if(nums[mid]>value) { right=mid-1; }elseif(nums[mid]

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

intBinarySerach(vector&nums,intn,inttarget){ intleft=0,right=n-1; while(left<= right) {         int mid = left+((right-left)>>1); if(nums[mid]>=value) { if(mid==0||nums[mid-1]

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

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

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

intBinarySerach(vector&nums,intn,inttarget){ intleft=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)) { returnmid; }else { left=mid+1; } } return-1; }

4、队列

例子:滑动窗口最大值

队列回忆:

火车站买票应该都经历过,窗口小姐姐每次服务排在最前面的那个人,买完票则从头部离开,后面人往前一步接替离开的人继续购票,这就是典型的队列结构。

计算机中的队列和其类似,先到先得,先入先出,每个元素从尾部入队,从头部处理完出队

队列定义

单调队列

假设将学生从高年级到低年级排列,随着时间的推移,高年级同学从队列头部毕业,低年级从尾部进入。大部分学校都有校队,假设小林高三,我高二,小七高一,小林毕业接班的是我,我毕业,很可能就是小七接班,而当我进队的那一刻,小七即使进的早但是战斗力没我高,所以小七是永远没计划被选中啦。所以,纵观全队,不仅有着队列的性质,也有着单调的性质,所以就是单调队列。

为什么需要单调队列

比较明显的作用是,用来维护队列处理顺序中的区间最大值。

高频面试题----滑动窗口最大值

滑动窗口没向后滑动一位,就有一个元素从队首出队,同时也会有个元素从队尾入队。这个题需要求区间的最大值:意味着需要维护在队列处理顺序中的区间最大值,直接上代码附上注释

#defineMAX_N1000 intq[MAX_N+5],head,tail; voidinterval_max_number(int*a,intn,intm){ head=tail=0; for(inti=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 ",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号元素小,这就是单调栈的性质。所以,单调队列是用来维护区最值的高效结构,单调栈呢是维护最近大于或小于的高效结构。下面看个例子

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

示例 合法

({})
{()}

示例 非合法

([)]
(((){}

publicbooleanisValid(Strings){ Stackstack=newStack<>(); Mapmap=newHashMap<>(); char[]chars=s.toCharArray(); map.put(')','('); map.put('}','{'); map.put(']','['); for(inti=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]成立

实现

#defineMAX_V10000 #defineMAX_N100 intv[MAX_N+5],w[MAX_N+5]; intdp[MAX_N+5][MAX_V+5]; intget_dp(intn,intW){ //初始化dp[0]阶段 for(inti=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 我的问题是,如何分配糖果,能尽可能满足最多数量的孩子?

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

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

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

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

#include #include #include usingnamespacestd; /* 解题思路: 遍历两边,首先每个人得一块糖,第一遍从左到右,若当前点比前一个点高就比前者多一块。 这样保证了在一个方向上满足了要求。第二遍从右往左,若左右两点,左侧高于右侧,但 左侧的糖果数不多于右侧,则左侧糖果数等于右侧糖果数+1,这就保证了另一个方向上满足要求。 最后将各个位置的糖果数累加起来就可以了。 */ intcandyCount(vector&rating){ intres=0; //孩子总数 intn=rating.size(); //糖果集合 vectorcandy(n,1); //从左往右遍历 for(inti=0;i< n - 1;i++) {         if (rating[i + 1] >rating[i])candy[i+1]=candy[i]+1; } //从右往左 for(inti=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() {     vectorrating{1,3,2,1,4,5,2}; cout<< candyCount(rating) << endl;     return 0; }

责任编辑:lq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 算法
    +关注

    关注

    23

    文章

    4615

    浏览量

    92976
  • 数据结构
    +关注

    关注

    3

    文章

    573

    浏览量

    40146
  • 链表
    +关注

    关注

    0

    文章

    80

    浏览量

    10570

原文标题:高频手撕算法合集来了!

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    面试题】人工智能工程师高频面试题汇总:机器学习深化篇(题目+答案)

    ,或者深度学习的框架,还有怎么优化模型,这些都是加分项,能有效提高面试通过率。本篇小编整理了一些高频的机器学习深化方面的面试题,这些题目都是从实际面试
    的头像 发表于 12-16 13:42 1966次阅读
    【<b class='flag-5'>面试</b>题】人工智能工程师<b class='flag-5'>高频</b><b class='flag-5'>面试</b>题汇总:机器学习深化篇(题目+答案)

    面试题】人工智能工程师高频面试题汇总:Transformer篇(题目+答案)

    随着人工智能技术的突飞猛进,AI工程师成为了众多求职者梦寐以求的职业。想要拿下这份工作,面试的时候得展示出你不仅技术过硬,还得能解决问题。所以,提前准备一些面试常问的问题,比如机器学习的那些算法
    的头像 发表于 12-13 15:06 518次阅读
    【<b class='flag-5'>面试</b>题】人工智能工程师<b class='flag-5'>高频</b><b class='flag-5'>面试</b>题汇总:Transformer篇(题目+答案)

    人工智能工程师高频面试题汇总——机器学习篇

    ,或者深度学习的框架,还有怎么优化模型,这些都是加分项,能有效提高面试通过率。本篇小编整理了一些高频的机器学习方面的面试题,这些题目都是从实际面试
    的头像 发表于 12-04 17:00 865次阅读
    人工智能工程师<b class='flag-5'>高频</b><b class='flag-5'>面试</b>题汇总——机器学习篇

    DDC264配置寄存器数据写入和320 DCLK时钟脉冲后的回读数据结构是什么?

    配置寄存器数据写入和320 DCLK时钟脉冲后的回读数据结构是什么? 根据注和表9,16位配置寄存器数据,4位修订ID, 300位校验模式,怎么可能有1024 TOTAL READBACK BITS, format = 0
    发表于 11-19 07:58

    视觉软件HALCON的数据结构

    在研究机器视觉算法之前,我们需要先了解机器视觉应用涉及的基本数据结构。Halcon数据结构主要有图像参数和控制参数两类参数。图像参数包括:image、region、XLD,控制参数包
    的头像 发表于 11-14 10:20 420次阅读
    视觉软件HALCON的<b class='flag-5'>数据结构</b>

    常见插元件识别

    常见插元件识别. 长按识别二维码关注[现代电子装联工艺技术]订阅号,开启我们共同的学习之旅 end 分享 收藏 点赞 在看      原文标题:PCBA丨常见插元件识别 文章出处:
    的头像 发表于 11-13 10:39 213次阅读
    <b class='flag-5'>常见</b><b class='flag-5'>手</b>插元件识别

    嵌入式常用数据结构有哪些

    在嵌入式编程数据结构的选择和使用对于程序的性能、内存管理以及开发效率都具有重要影响。嵌入式系统由于资源受限(如处理器速度、内存大小等),因此对数据结构的选择和使用尤为关键。以下是嵌
    的头像 发表于 09-02 15:25 494次阅读

    电路怎样消除高频干扰

    在电子电路设计高频干扰是一个常见的问题,它可能导致电路性能下降、数据传输错误甚至设备损坏。因此,消除或减少高频干扰是电路设计
    的头像 发表于 08-22 11:05 1899次阅读

    面试嵌入式工作,会被问什么问题?

    面试嵌入式工作时,面试官可能会从多个方面考察应聘者的知识、技能和经验。以下是一些常见的嵌入式工作面试问题,这些问题涵盖了基础知识、专业技能、项目经验和个人能力等方面
    的头像 发表于 07-17 09:26 2032次阅读
    <b class='flag-5'>面试</b>嵌入式工作,会被问什么问题?

    揭秘编程核心:基本数据结构算法思想详解

    描述问题的数据除了各数据元素本身,还要考虑各元素的逻辑关系,主要是一对一的线性关系,一对多的树型关系和多对多的图形关系。
    的头像 发表于 04-25 11:51 1096次阅读
    揭秘编程核心:基本<b class='flag-5'>数据结构</b>与<b class='flag-5'>算法</b>思想详解

    探索编程世界的七大数据结构

    结构就像是一颗倒挂的小树,有根、有枝、有叶。它是一种非线性的数据结构,以层级的方式存储数据,顶部是根节点,底部是叶节点。
    的头像 发表于 04-16 12:04 390次阅读

    TASKING编译器是否可以将数据结构设置为 \"打包\"?

    复制到相应的数据结构,应用程序就可以节省解包时间。 是的,我知道访问打包数据结构在内存访问时间性能方面并非最佳。 不过,至少在我的应用,对这种打包
    发表于 03-05 06:00

    矢量与栅格数据结构各有什么特征

    矢量数据结构和栅格数据结构是地理信息系统(GIS)中最常用的两种数据结构。它们在存储和表示地理要素上有着不同的方法和特征。在接下来的文章,我们将详细介绍这两种
    的头像 发表于 02-25 15:06 2635次阅读

    数组和链表在内存的区别 数组和链表的优缺点

    数组和链表在内存的区别 数组和链表的优缺点  数组和链表是常见数据结构,用于组织和存储数据。它们在内存的存储方式
    的头像 发表于 02-21 11:30 1055次阅读

    区块链是什么样的数据结构组织

    区块链是一种特殊的数据结构,它以分布式、去中心化的方式组织和存储数据。区块链的核心原理是将数据分布在网络的各个节点上,通过密码学算法保证数据
    的头像 发表于 01-11 10:57 2268次阅读