- 博客(82)
- 收藏
- 关注
原创 位图——求两数之和
因此,我们可以用a&b找到两数要进位的位置,但进位并不是在当前位,而是在进位的前一位。然后我们再通过a^b得到两数的无进位相加的位置。把a^b看成a,(a&b)<<1看成b。直到a&b==0完毕(无进位了,a^b就是两数之和)。我们在讲^运算时有一个记法是无进位相加,也就是如果两个都是1,那么1+1要进位此为变成0,但是我们不做真正的进位(前一位不改动),然后0和1的位数相加为1(自然无进位不做处理)。因此我们发现,只有二者都为1的情况下该位才进位,否则都不处理(有点类似于&的运算律)
2025-05-24 14:36:34
131
原创 位图算法——判断唯一字符
我们可以创建一个整型变量(32位)而一共才26个字母,所以我们只要用到0-25位即可,0代表没有,1代表有,先判断该位是否为1,为1直接返回false,为0把该为标记为1进行下一次判断。这道题有多种解法,可以创建hash数组建立映射关系判断,但不用新的数据结构会加分,因此我们有“加分”办法——用位图。可优化的细节:如果字符串长度>26直接不符(鸽巢原理)
2025-05-22 21:48:44
141
原创 位运算及其算法
计算机中的所有数在内存中都是以二进制形式进行存储的 ,位运算就是直接对整数二进制位进行操作,有些时候在程序中使用位运算进行操作,会得到极高的便利性。我们以int整型为例,每个int占4个字节32个bit位。对于无符号整数,这32位的0、1都可以用来表示数字。而对于有符号整数,其最高位的bit位代表着整数的正负,0为正,1为负。,范围自然就小很多。无符号整数可表示的范围(32位):0~2的32次方-1(4,294,967,295)
2025-05-22 21:30:09
663
原创 前缀和——和为K的子数组
如果我们正着看,就是0位置从前向后遍历,然后0开始的遍历完再从以1位置为开始的遍历,在我们本题中的遍历方法,无非就是把i作为了结尾,然后从右向左,也可以实现枚举所有子数组。显然,暴力我们是不可取的,但这里我们可以采取一种新的遍历数组形式,从后向前,也就是以i位置为结尾的所有子数组,这个子数组只统计i位置之前的。然后,问题就转换成了,我们要找到以i位置为结尾的所有子数组,且在[0,i-1]内,有多少个前缀和为sum[i]-k,因此,如果上面的成立,一定在此之前就已经被统计过了。
2025-05-19 20:57:21
352
原创 前缀和——中心数组下标
定义两个数组f,g。f[i]表示[0,i-1]所有元素的和 f[i]=f[i-1]+nums[i-1];g[i]表示[i+1,n-1]的和。此题我们不应局限于前缀和的模板,因为该中心下标把数组分为两个部分且每个部分都要求和,我们就一个再创建一个”后缀和”g[i]=g[i+1]+nums[i+1];因为依靠关系,f要从左到右,g要从右到左。注意题干中的边界条件,0和n-1位置出的左、右是0,因此不要越界。
2025-05-18 22:31:06
184
原创 ”一维前缀和“算法原理及模板
通过这个公式我们就可以说明为什么下标需要从1开始了,如果l为0,也就是想求从最左到r,那么公式里就是dp[r]-dp[-1]。现在的问题就是:如果我们用暴力枚举来记录每次l与r之间的和,那么肯定是会超时的(时间复杂度O(N*q)),我们要另辟蹊径。注意,我们这里的arr和dp中的i都是以1开始记录而不是0,稍后我们解释一下原因,我们先把arr[0]和dp[0]都看成0。这个dp数组的每一个元素dp[i]记录着arr[i]及之前的元素之和。假设我们要求l-r的和,只需要用dp[r]-dp[l-1]即可。
2025-05-16 18:26:20
357
原创 经典算法“二分查找”你能写明白吗
如果要查找数组中间的元素——直接朴素二分如果要查找左右端点,首先判断条件必须是<。如果是左端点,要分成小于和大于等于的区间,然后left=mid+1或right=mid。中点选取采用“不加1”形式如果是右端点,要分成小于等于和大于的区间,然后left=mid或right=mid-1。中点选取采用“加1”形式。
2025-05-15 19:29:38
754
原创 滑动窗口——字符串中所有异位词
我们定义一个变量count来统计滑动窗口中有效字符的个数,以p为例,abc分别只出现一次,那么s用双指针存放时,先判断一下,如果存放的是有效字符且此时该有效字符的存放次数小于等于p中该字符的个数就让count++,这是进窗口的操作。把p的字符串存在hash1中(记录p的长度),把s存放在hash2中,用双指针进行存放,当存放长度≤p.size()就进行比较(hash1,hash2)。此方法可行,但我们发现,每次进行二者比较时,都需要遍历26次才能完成一次比较,我们可以把比较方法进行优化。
2025-05-14 10:20:21
199
原创 滑动窗口——水果成篮
我们需要借助hash表来记录不同数字出现的种类以及每个数字出现的次数。然后先让right数字放hash,判断,如果种类小于等于2,重复上述操作,如果大于2,left++,并在hash中删去对应次数。直到遍历完全(right<size)。根据题意我们转化一下,在数组中求一个最长的子数组,数组中的数字种类不超过2种。借助暴力思想,我们定义双指针进行进窗口、判断、出窗口、更新结果的流程。
2025-05-13 23:28:24
197
原创 滑动窗口——将x减到0的最小操作数
这个题如果我们直接去思考方法是很困难的,因为我们不知道下一步是在数组的左还是右操作才能使其最小。正难则反,思考一下,无论是怎么样的,最终这个数组都会分成三个部分左中右,而左右的组合就是我们进行的所有操作(极端情况下左或右长度为0),但不管怎样,都分成了连续的几块。定义同向双指针,然后“进窗口”,判断(sum是否>target,此处的sum是滑动窗口内之和,target是上面的sum-x),如果满足条件,则出窗口,然后更新结果,判断此时的sum是否=target,符合条件,记录长度,然后一次次取最大值。
2025-05-11 23:41:31
211
原创 滑动窗口——无重复字符最长的字串
但这个题我们有更优化的方法,利用数组代替hash(重点不在这!我们用滑动窗口的原理(同向双指针),先让left和right指向头,然后判断right所对应的数组下标是否为1(利用标记来达到桶的效果初始全为0),如果为0则标记为1,right++;如果为大于1,先更新结果,left++,然后right++,这里right不重新回到left再遍历是我们已经能证明二者之间一定无重复字符了。直到right走到尾。题意不难理解,这个题我们暴力枚举的思路是把每一个字符遍历存到hash桶中,如果放两次就进行结果更新。
2025-05-08 00:06:03
557
原创 滑动窗口——长度最小子数组
二者之间的区域我们就可以看成窗口在之间滑动,left每移动一次(符合条件的子数组),就记录一下当前的窗口长度。我们发现,滑动窗口对于此题就相当于帮我们省去没必要的遍历,就以上图为例,此时sum=8,那么我就没必要枚举让right右移的所有子数组了(枚举的话虽然也一定符合target但是长度不是最短)。根据滑动窗口,我们来说一下思路:先定义两个指针指向头,然后left不变,每次对right及之前的数字求和然后right++(求得left和right之间的和)。
2025-05-07 17:05:23
210
原创 双指针——四数之和
不同的是,我们之前是定一个数,现在需要定两个数(两层for)然后思路不变。且之前不需要我们传参(因为指定和为0),现在指定和需要我们自己决定。我们可以用target-nums[i]-nums[j]作为一个整体,然后就又变成了三数之和。这个题我们可以套用“三数之和”的原理。
2025-05-06 09:07:20
210
原创 双指针——三数之和
至于如何去重,我们可以在指针移动前加一条判断,看移动后的数是否和移动前的数相同,若相同就跳过。(固定的数也需要去重)走两步就要注意越界的问题。这里和有效三角形不同的是,我们固定一个数时,找到一组数不要停,继续缩短区间找下一组,直到两指针相遇。这里有个小优化:如果a>0则直接跳出循环(在a的右侧都是大于0的数,一定找不到符合的组合)先把数组排序,然后固定一个数字a,在该数后面的区间内,利用双指针找到两数之和为a。题目中有一个重点:不能包含重复三元组,解决这个问题最好的方式——去重!
2025-05-06 08:29:43
204
原创 双指针(6)——和为s的两个数字
相加大于s:说明二者之间任一数与right组合都不满足,right--相加小于s:说明二者之间任一数与left组合都不满足,left++
2025-05-04 01:06:04
204
原创 双指针(5)——有效三角形个数
我们先计算arr[left]+arr[right](1+9=10,不构成),如果满足a+b≤c,那么left和right的中间任何一个数和left组合都不能满足三角形,所以此时我们就可以把所有数与left的组合都忽略(不用计算了)即,让left++,这是一次流程。反之如果大于c,那么此次流程中的所有数与right结合都满足要求,则有效三角形个数为right-left,然后让right--进行下一次流程,直到left与right相遇。这道题我们首先可能会想到暴力解法,三个for循环然后进行check()。
2025-05-02 20:30:07
364
原创 Linux——线程(3)线程同步
条件等待是线程间同步的⼀种手段,如果只有⼀个线程,条件不满足,⼀直等下去都不会满足, 所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好 的通知等待在条件变量上的线程。通过上面的抢票系统我们发现,有的线程,进行工作(挂锁),当其马上结束工作(解锁),发现外面有很多线程在排队等着加锁执行任务,这个线程解锁后就立马给自己加锁(可以想象成自己能直到解锁的时间,所以加锁的优势比其他线程大),但是如果这个线程本身并没有做什么工作,就会使工作效率低下,线程饥饿等问题。
2025-05-01 17:30:32
821
原创 双指针(4)——盛水最多的容器
这里首尾比较去小的原理是,在某数组中,我们能确定在该容器宽度变化时,数组的高不变,去宽度最大作为该组的最大值,然后缩短数组(去小)继续重复此操作。那么我们就可以认为,在这几个数遍历到4时,从左到右是越来越小的。也就是我们就没必要再去遍历4(把4删除),因为我们是要求储水量最大值,接着在(6,2,5)之间遍历。如果我们此时让“4”去向内遍历(只看每一个数和4遍历的比较),根据木桶原理,这个容器的高不变,宽度会越来越小,即蓄水量越来越少。这题可以暴力枚举,但会超时,所以我们要用其他方法。
2025-04-30 21:49:03
233
原创 双指针(3)——快乐数
定义两个指针,让后面的指针在数组中遍历,前面的指针先不动,符合题目要求时,让两个指针的数据进行交互,直到后面的指针走到数组结尾。但这里并不是链表,也没有指针。我们并不一定要使用指针,只需要每次把一个数看成指针即可。我们看似是两种情况,实质上是一种情况,都会进入无限循环,只是其中一个循环一直是1。这和判断链表是否有环类似,这里我们使用的方法是:快慢指针法。
2025-04-28 10:27:20
160
原创 Linux——线程(2)线程互斥(锁)
这就需要我们最上面的概念——原子性,在ticket--的过程中一共有三步:把ticket从内存放在CPU,CPU进行--,再放回内存,这是对于每一个线程的过程,但是在这个过程中,如果我们把数据放进CPU那么这个数据就对于线程是私有的了(本身的ticket是全局变量所以共享资源),他们完成第一步时,这时线程突然切换了,结果到下一个线程本来应该拿到比上个线程少一的ticket,但因为上一个还没有--所以拿到的值是一样的,然后当他们都进行--,就会出现为负数的原因。线程在申请锁的时候,其他线程要进行阻塞等待!
2025-04-27 21:18:26
863
原创 双指针算法(2)——复写零
定义两个指针,让后面的指针在数组中遍历,前面的指针先不动,符合题目要求时,让两个指针的数据进行交互,直到后面的指针走到数组结尾。解决方法:我们发现出现这种错误的cur最后指向的一定是0,所以我们先让n-1等于0,然后,dest-=2,cur--。这个题,如果我们用两个数组下标去标记,一个遍历,一个交互(赋值),就会造成下面的结果。其实,从前向后不行,从后向前可以,我们通过结果直到,数组的最后一个是4.第一次复写时,发现数据已经被覆盖掉了,所以不可取。但也有特殊情况比如:[1,0,2,3,0,4]。
2025-04-27 21:15:50
270
原创 双指针算法(1)——移动零
交互步骤:首先让dest处于-1的位置,让cur向后遍历,当cur指向0,cur++,直到cur走到非0,交换dest+1与cur,然后dest++,cur++。定义两个指针,让后面的指针在数组中遍历,前面的指针先不动,符合题目要求时,让两个指针的数据进行交互,直到后面的指针走到数组结尾。我们要让“两个指针”把数组进行分块。(注:形成[0,dest]的原因我们可以想一下数组全非0的时候,那么就是每次自己和自己交换直到走到最后)我们需要让第一个分块全部为非0,第二个分块全部为0,第三个分块是未遍历。
2025-04-27 10:42:22
377
原创 Linux——线程(1)线程概念与控制
上面提到过,这并不是系统调用而是一个库,是用户级别的线程库。参数 :thread: 返回线程 ID (输出型)attr: 设置线程的属性, attr 为 NULL 表示使用默认属性start_routine: 是个函数地址,线程启动后要执行的函数arg: 传给线程启动函数的参数arg参数可以是任意类型(变量,数字,对象等)成功返回0,失败返回错误码。
2025-04-25 10:21:38
920
原创 Linux——信号(2)信号保存与捕捉
就以键盘为例,当我们的按键按下了,键盘的外设就会发起中断,实际上我们的外设并没有直接连接CPU,而是连接到了一个叫中断控制器的东西,当收到中断时,它就会知道是哪个设备发出并知道中断号,然后控制器通知CPU发生中断,此时CPU就可以获得对应的中断号。上次我们提到,PCB中有一个记录信号的表(位图),每一个比特位记录着对应的信号,这个表叫pending,除此之外还有两个表handler和block,handler就是记录每一个信号的递达方法,是一个函数指针数组,每一个下标代表着每一个信号的处理方式。
2025-04-21 22:30:05
700
原创 Linux——信号(1)信号的产生
在生活中,我们也会有很多信号,比如下课铃响了我们知道下课了,绿灯来了我们知道该走了,也就是说,它们在向我们发出某种消息,使得我们去做一些事情,在操作系统中也类似,信号就是其他进程,向目标进程发送异步事件的一种方式。所谓异步,就是指目标进程自己也不知道这个信号什么时候来,突然、随时的情况。二、关于信号你需要知道的一些知识1.我们如何识别信号呢?识别信号是内置的。进程认识信号,是程序内置的特性。就相当于我们从小就被告知红灯停绿灯行2.信号产生后该如何处理知道吗?如果没有产生,信号又该如何处理知道吗?
2025-04-17 23:11:32
768
原创 Linux——进程通信
我们知道,进程具有独立性,各进程之间互不干扰,但我们为什么还要让其联系,建立通信呢?比如:数据传输,资源共享,通知某个事件,或控制某个进程。因此,让进程间建立通信是必不可少的。但如何通信呢?根据进程的独立性,我们需要保证,让不同的进程看到同一份资源!但这个资源若是进程a建立的则b看不到,b建立的a看不到,因此,这份资源必须由操作系统提供!
2025-04-16 21:53:39
673
原创 Linux——文件(3)软硬连接和动静态库
也就是说,如果实际的偏移量是0,那么这些就代表着真正的物理地址,如果是a,那么真正的物理地址就是每一个地址加上偏移量a。我们在进程地址空间部分提到过进程启动时建立PCB与内存关系的映射过程,其中,动态库也是可以映射的,其具体机制为,先从磁盘中获取动态库到内存,然后通过页表把动态库映射到虚拟地址空间的共享区部分,然后我们的task_struct就可以找到对应的库了。在CPU的结构中,有一个叫CR3的寄存器,其记录的是物理地址,它拿到这个物理地址,就会去页表找对应的虚拟地址,进而找到对应的代码并执行!
2025-04-10 00:30:51
505
原创 老师们一笔带过的那些“痛“——缓冲区
我们的计算机想处理数据的信息,必须要交给CPU,而我们的键盘、显示器都属于外设,外设的运行速度相比于CPU是要慢很多很多的,如果我们每一次输入都会让外设和CPU交互的话,那么整体运算速度肯定是接近于外设的,为了提高效率,我们既要完成最初的输入输出操作,同时也要减少交互的次数,即我们在内存留出一块空间,先接收一定量的外设带来的数据,等到收到“信号”再一次性交给CPU。这就是行缓冲的原理。在我们的使用中,我们好像觉得,这个打印函数好像是运行起来就打印了啊,好像看不见缓冲区在其中干了什么,我们看下面的代码。
2025-04-03 20:17:38
945
原创 Linux——文件(2)文件系统
但是,当我们想知道一个文件名时,我需要知道它在哪个目录的数据块下,可是目录也有名字,我们也需要知道目录的名字,一直递归下去,就是路径的逆向解析,知道找到根目录,因为根目录是写死的,我们也就知道文件的具体位置了,所以我们讲,文件必须要有路径。盘片上的每一个扇区看似没有什么存储规律,但我们可以类比于磁带,磁带上面的长条就记录着数据,我们就可以把数据看成线性结构,而磁盘上的数据,我们就可以视为是磁带绕中心一圈圈卷起来的盘,如果把其拉长也可以看成线性结构存储,也就是数组,这样我们就可以通过数组下标定位数据扇区了。
2025-04-03 15:30:01
520
原创 Linux——文件(1)文件打开与关闭操作
而我们要处理文件的信息等,需要把其交给CPU处理,但根据冯诺依曼体系中,我们的CPU是无法直接访问磁盘的,所以我们只能把文件放入内存中,才能被CPU调度处理。实际上,我们每一个文件的struct file中除了有自己的文件信息,还有自己的操作表,里面是函数指针集合(用来完成各种操作的函数),同时,还有一个文件的内核缓冲区,当我们用write写入时,本质是把write中的字符串拷贝到缓冲区,然后等关闭文件时,对文件缓冲区进行刷新即可完成写入,所以write其实是一个拷贝函数,这便是写入的过程。
2025-03-31 15:50:44
1040
原创 Linux——进程控制
第二个参数,就可以获取我们子进程的退出信息了,但这个参数是一个输出型参数,是希望我们传参然后通过操作系统把信息带出来,如果这里传空指针,代表我们不关心它的退出信息,回收即可,如果想查看,我们可以随意创建一个变量(不能是全局变量)然后把其地址传参。我们一般用0表示成功,非0表示运行错误。此外,进程有时还会出现异常终止的情况,比如野指针等非法行为,我们上面说的退出,要么是正常运行完毕,要么是结果不对,至少代码的整个流程都执行了,而这种情况是直接终止代码运行,是操作系统提前使用信号终止了进程(kill命令)。
2025-03-27 08:03:51
921
原创 Linux——进程(5)进程地址空间
我们的野指针就是属于访问虚拟地址所对应的不存在的地址而报错。当创建子进程时,会拷贝父进程的代码和虚拟空间,实际上此时指向的物理空间也是一样的,也就说如果我们不做数据的修改,父子的数据也是共享的,但当我们修改时,子进程的物理内存讲改变(重新找一个地方),但此时的虚拟地址不变,也就导致了我们一个虚拟地址指向了两个物理地址。结果我们并不意外,只是我们发现,父子进程的gval的地址是一样的,这有点颠覆我们的认知,同一个地址,怎么会有两个不同的值,我们只能认为,这个地址不可能是物理意义上的地址,我们叫虚拟地址。
2025-03-19 21:55:43
374
原创 Linux进程——(4)命令行参数、环境变量
结果显示argc为1,argv[0]是./code(一个字符串),但当我们输入./code 1运行时,agrc变成2了,argv[1]为1,argv[0]不变,当我们输入./code 1 2运行,argc为3,argv[2]为2。我们也可以用export i=20直接放入环境变量,我们知道,我们启动的进程,是从父进程来的,因此环境变量是可以被子进程继承的,本地变量不能。(对于我们平常的程序都是子进程,他们的父进程是bash,当然,也可以被我们fork出的所有子进程看到使用,所以体现了全局性。
2025-03-18 21:19:38
958
原创 Linux---进程(3)进程优先级、切换与调度
想象一下,当我们新建一个进程时,一般来说优先级都是比较高的,所以就会排在队列前,这就导致一个问题,如果我新建太多进程,导致后边优先级低的进程很久才可以被调度就会造成不均衡的问题,因此,我们要保留原来的调度次序,即把新建的进程放在expired的队列中,时间片到了的未执行完毕程序也会放入expired队列,这样就会使active中的进程越来越少,当调度完毕后,只需要swap两个队列的数据既可重新进行调度轮转。因此,每一个队列还有一个额外的变量,用于记录当前队列的进程数,当少于一定数量时就交换。
2025-03-17 10:19:15
629
原创 Linux——进程(2)进程的状态
因此,等待的本质是进程连入目标外部设备,CPU没调度。字面意思,就是父子进程运行时,退出父进程,由上面的理论得知,父进程会被回收(此时是观察不到父进程的Z状态的,因为父进程由操作系统管理,被bash瞬时回收了),而子进程会处于一种领养的状态,我们还发现,当变成孤儿进程时,子进程的ppid变成了1,这个1也就说systemd,系统。s:休眠状态,也是阻塞等待的状态,虽然在进行运行中我们看着好像是r状态,但一旦有输入输出函数时,运行时间的大部分都是和不断的和外设交互,很少是被CPU调度(因为CPU太快了)。
2025-03-13 21:29:09
892
原创 Linux——进程初步
这个PCB就是操作系统管理进程的方式。其实就和银行的运作原理是类似的,我们到银行办理业务,正常来说要找业务员进行办理,但实际上,如果我们知道办公的原理,跳过业务员这一步自己办理也是可以的,但是这会大大增加风险,我们不能保证每个人都能顺理成章地完成自己的工作,难免会有失误,因此银行为了避免这种情况,直接封死了客户自行办公的途径,同时又开放了一个新的路径——业务员窗口办公,这样就可以了。我们可以用getppid函数获得父进程的id,我们在多次运行后发现,一个进程,它的pid是随机的,但ppid是一直不变的。
2025-03-12 21:23:56
619
原创 Linux——工具(4)gdb调试器
1.我们所编写的程序,有debug和release版本,其中前者是添加调试信息的,后者就没有,我们一般写c/c++都是debug版本,但在Linux中,我们用gcc/g++所编译的文件都是release版本的,但我们的gdb需要我们以debug版本发行,所以我们需要改动一下,只需要在原来的gcc指令后面加-g选项即可。gdb是干什么用的,它是一个调试器,相当于我们vs软件中的各个操作的集合软件,比如逐语句运行,打断点等等。删除断点的命令为d,但d后面跟的不是行号,而是我们的断点编号。
2025-03-10 18:12:30
738
原创 Linux——工具(3)git——版本控制器
其实,本地仓库确实可以对我们本地写的内容进行永久性的保存,但是如果某天你的本地机器出现了故障导致无法使用,那么即使保存我们也无法查看和获取了,这时候远程仓库就起了作用,无论你的本地出现了什么状况,我都可以随时保证你可以查看内容,只需要本地换一台机器然后重新进入仓库即可。我们建议:在上传至仓库的代码不上传过程文件,这是为了保证仓库的整洁,所以,为了保证我们上传的文件都是有效的,我们的本地仓库有一个隐藏文件.gitignore,凡是以这些为后缀的文件都不允许上传。创建好仓库我们就可以把仓库拷贝到我们的本地了。
2025-03-09 21:08:31
1008
原创 Linux——工具(2)makefile
一般来说我们修改文件的属性,往往变动的只有modify(在此我们不讨论access因为访问的次数太多导致时间变化太频繁),但当我们修改内容时,也就是change变的情况下,我们发现modify往往也会变化,这是因为内容往往会和属性联动。我们假设现在编写一段代码并编译形成可执行程序,那么当我编写的时候相当于修改内容的时间,形成可执行程序相当于修改属性的时间,这两个时间几乎不可能完全相同,机器就会识别,如果修改属性的时间早于修改内容的时间,说明我之前编写的内容已经形成可执行程序了,不会再形成新的了。
2025-03-07 17:51:08
671
空空如也
空空如也
TA创建的收藏夹 TA关注的收藏夹
TA关注的人