本章节会依次收录在学习过程中遇到的经典leetcode与牛客算法题,不间断更新,希望我们可以一起在算法的海洋中,解决一个又一个挑战,迎接头脑风暴吧!
本章节会依次收录在学习过程中遇到的经典leetcode与牛客算法题,不间断更新,希望我们可以一起在算法的海洋中,解决一个又一个挑战,迎接头脑风暴吧!
位运算
1.消失的数字
对于这道题呢,其实我们首先的想法肯定是差了谁给他补上就好了,所以我们可以采用先去将0-n的所有数相加,再减去nums数组中的数字不就好啦,所以我们很容易得出这种解法
但是我们细细的想一下,这样的解法真的是完美的吗?,我们的代码中是有0到n的加法的,那万一这个n特别大,超出了int的范围,溢出32位了?此时便会出现越界问题,所以这个方法并不完美
那么我们应该如何做才能避免越界呢?我们可以很轻易的想到用位运算,我们可以利用原数组与0到n所有数异或这种方式,在位上解决,由此我们可以得出第二种解法
这种方式不会出现越界,是比较严谨的
2.只出现一次的数字
当我们拿到这道题时,首先想到的是就是异或,对所有的数进行异或,相同的消去,余下的那个就是答案
其实这道题也有很多其他解法,比如排序啊,哈希啊,但是对于时间复杂度而言,最优的一定是最接近底层的算法,这里就是异或算法
数组中数字出现的次数1
我们拿到这道题时,开始时有些懵的,我们也能想到我们的异或操作,但是如果全部异或,虽然可以将我们其他所有不相关的数消除,但是我们结果的那两个数字,也会被异或,最后得到的就是他们两个的异或结果,那么如何去将他们分开呢?我们仔细想了下,其实可以通过去寻找他们的不同点,来解决,我们的异或操作是在位上运算的,所以其他操作最好也在位上进行,那么他们两个有什么不同呢?我们经过观察,其实可以想到,他们的二进制码一定是不同的,即使前几位相同,总有不同的那个,我们就可以从不同的这一位入手,但是我们又不能对他们单独操作,所以将整个数组按这个不同的位,分成两组,再进行异或,两组中其他的数一定都是成对出现的,所以也被消去了,就剩下了这两个数,就是答案
这便是我们的解法,值得注意的是,利用&1可以去查找位的值,而后对num进行移动,右移动多少位,其实就相当于1左移多少位,就可以得出那一位的值(不能去移动1,1后面移动会变成0,影响结果)
数组中数字出现的次数2
这道题也是一个变形题,如果用我们传统的异或也是不能去直接解决的,那么我们就从不同点入手,可以发现的是,所有的数字,除了目标数字都出现了3次,那么他们有什么共同点呢?没错,就是与3有关系,如果将所有的数的每一位分别加起来,出现三次的数,一定是可以被3整除的,而那个出现1次的数,它二进制位中如果是1的话,加上去就会使得这一位无法被3整除,那么只需要将所有的位上的数分别相加,无法被3整除的位置1,最后得到的就是结果
值得注意的是,移位时上面部分是将nums右移i位,相对的相当于1左移i位,到了下面就需要将1左移i位来进行数字的还原
顺序表
移除元素
这道题思路其实很简单,就是依次遍历,当遇到目标值,使得其后面的都前移,因为我们这道题求的是新数组长度,所以可以采用双指针,当取到非val的值时,src与des都++,取到val值时,仅src++,而后将nums[dst]赋给nums[src],这样的操作就可以使得在src值不断赋给dst时,
这道题关键用好双指针,了解指针走向,判断对状态即可
删除有序数组中的重复项
我们在拿到这道题,首先可以想到的就是直接遍历,同上一道题一样,用两个指针去操纵数组,但是我们会发现一个问题,要是遇上连续相等的,交换过后又没法判断后面的了,所以这道题我们两个指针不够了,需要3个指针来解决
虽然我们利用三指针解决了问题,但是其实有更简单直观的做法
快慢指针
slow指针慢走填充结果数组,fast指针通过与fast-1比较,相等则将fast赋给slow,进而将slow++,不断移动fast去搜索,直至结束
数组形式的整数加法
这道题我们采用我们一开始就可以想到的方法,重新开辟一个数组,将k入数组后反转,而后从后往前逐位与num相加,注意设置一个carry值,来存储加后的进位问题,还需要考虑某一个加完了,另一个没加完,两个都没加完的问题,当所有数加完后留下来了carry,还需要加上1,最后反转tmp数组
总的来说,这道题思路很直观,不过需要注意细节,要对拆分数字也有一定认识
旋转数组
其实我们拿到这道题,我们首先反映的就是,数组左边的右移k,右边的填到前面,但是这样做其实是有些麻烦的,不好控制,会将右边原来的数覆盖,就拿不到了,所以我们采用借助新数组,从第k位开始,依次填入新数组,而后拷回原数组即可
这边是我们的基本思路,但是其实这样有耗费,有O(n)的空间复杂度,所以还有一个更为巧妙地方法,原地旋转
思路就是先对整体数组旋转,再对0到k-1,最后k到end,就完成了,很神奇,很厉害
链表
移除链表元素
这道题其实就是我们的删除链表,不过这里需要返回新的头节点,所以我们需要先创建一个节点连在头节点前面,最后返回新节点的next即可
反转链表
这个题呢,显而易见的思路便是利用三个指针对其进行操作
初始化prev为nullptr,便是我们反转过后的尾结点,而后依次移动,cur指向nullptr,next保存下一节点,直到cur走到空,此时prev所对应的便是反转过后链表的头
链表的中间节点
这道题其实就是最经典的快慢指针做法,不过值得注意的是,边界条件,当fast指针走到null,仅代表的是链表单数这种情况,还需要补一个fast->next为null时,包含两种情况才可以
倒数K个结点
其实我们拿到这道题,先思索了下,可以先遍历一遍,计数,找到第K个结点,然后再次遍历到第K个,输出后面的结点,但是这样时间复杂度很高,显然不好
我们这道题采用双指针的做法,先让fast指针前移k个结点,而后再让slow与fast同时移动,当fast移动到结尾时,slow正好移动到第K个,输出结点即可,不过值得注意的是,对于中间fast移动k步时,考虑k大于总结点数的情况,此时fast提前走到了null,输出null即可
合并两个有序链表
针对这道题呢,其实我们思路很简单,就是那个大连哪个,不过有许多细节需要注意,首先,我们判断是否存在链表为空,为空则返回另一个,其次,我们设置新的表头与cur指针,判断l1与l2小的那个,设为新表头,而后开始遍历链表,注意,此时当存在链表结束,我们就结束,将剩下的另一个链表直接连上即可,遍历链表,取小的链到cur后面,而后后移l1或l2,最后当有链表走完,直接将另一个链到链表中,返回头节点即可
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1==nullptr)
{
return l2;
}
if(l2==nullptr)
{
return l1;
}
ListNode* phead=nullptr;
ListNode* cur=phead;
if(l1->val<l2->val)
{
phead=cur=l1;
l1=l1->next;
}
else
{
phead=cur=l2;
l2=l2->next;
}
while(l1&&l2)
{
if(l1->val<l2->val)
{
cur->next=l1;
l1=l1->next;
}
else{
cur->next=l2;
l2=l2->next;
}
cur=cur->next;
}
if(l1)
{
cur->next=l1;
}
if(l2)
{
cur->next=l2;
}
return phead;
}
};
划分链表
emmm,其实我们看到这道题,想到的就是较为暴力的解法,他要将链表分为两个,那我们就创建两个链表,一个接受小于x的,一个接受大于x的,最后将这两个链表链接,并链接到head后,返回head即可
链表回文
其实我们拿到这道题,可以想到的最简单的解法是将其输进数组中,然后判断,不过我们这道题没有采用这种方式,我们采用栈+链表,将链表整个入栈,然后弹出一半元素比较,全部相等则说明回文
再来演示数组解法
相交链表
在我们开始拿到这道题时,是有点蒙的,但是我们静下来想一想,他们相交的条件是什么呢?就是他们循环走一遍的距离是不一样的,长的那个和短的那个走的差值,再让长的提前走一下,然后再让他们同时走,相等不就找到了么,我们先来看下代码
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* A=headA;
ListNode* B=headB;
int a=0;
int b=0;
while(A->next)
{
A=A->next;
++a;
}
while(B->next)
{
B=B->next;
++b;
}
A=headA;
B=headB;
int gap=abs(a-b);
if(a>b)
{
while(gap--)
{
A=A->next;
}
}
else
{
while(gap--)
{
B=B->next;
}
}
while(A!=B)
{
A=A->next;
B=B->next;
}
return A;
}
};
虽然实现了,但是效率很低,我去看了下别人的做法,很厉害,当A,B指针走的时候,如果不相交,则其一定不同时到达null,若其相交,则一定同时到达null,此时我们就可以对其进行处理,当A,B分别在未相交的情况下走到null时,分别使A回到headB,B回到headA,再次走,此时他们一定会同时走到相交处,因为都走了一样的A+B的路径,最终一定会交到一起,下面是代码
很是厉害
环形链表
看到环形链表,我们应该怎么做呢?这其实也是个链表结点相交的问题,我们可以想到用快慢指针的做法,首先我们可以假设其有环,当slow入环的时候,与fast的距离为N,那么我们的slow指针每次走一步,fast指针每次走两步,差值就是每次减一,N,N-1,N-2.....所以如果有环,他们必定在环中相遇,如果没有环,则fast/fast->next指针一定会直接走向null
完成了基本的环形链表,我们来了解下进阶
这个题目要求的是返回环的入口,其实我们这道题是一道数学题,我们可以列出下列模型
根据上面我们推得的公式,可以得出,当一个指针从相遇点出发,一个指针从出发点出发,满足一个走L,一个走N*C-X时正好在入口点相遇,不过此时的指针每次都是走一步的,根据这个信息,我们就可以解这道题了
复制带随机指针的链表
这道题我们需要找规律,先创建新节点在源节点的后面,创建完新节点后开始链接random指针,我们发现,cur的random的next其实就是后面我们要的,cur->next->random,新节点的random,而后再将两个链表分开即可
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head==nullptr)return nullptr;
Node* cur=head;
while(cur)
{
Node* newnode=new Node(cur->val);
newnode->next=cur->next;
cur->next=newnode;
cur=cur->next->next;
}
cur=head;
while(cur)
{
if(cur->random==nullptr)
{
cur->next->random=nullptr;
}
else
{
cur->next->random=cur->random->next;
}
cur=cur->next->next;
}
Node* tmp=new Node(-1);
Node* curnew=tmp;
Node* curold=head;
while(curold)
{
Node* next=curold->next->next;
curnew->next=curold->next;
curold->next=next;
curnew=curnew->next;
curold=curold->next;
}
return tmp->next;
}
};
对链表实现插入排序
我们拿到这道题,首先要了解插入排序的原理,其实就是去控制指针,小的放在sorthead前面,不断前移,大的放到后面,后移,直到找到临界点,然后连接指针
栈与队列
有效的括号
我们拿到这道题时,开始时有一些没有头绪的,但是我们经过仔细想了下,发现如果利用栈,就会变得比较简单,我们的思路是这样的,扫描数组,将左括号入栈,右括号不入栈,如果其不是左括号,那么一定需要和栈顶元素匹配,如果不匹配,则这个式子就是错的,当我们进行判定是成立时,就需要出栈了,去判断之前的括号,当我们走完数组,如果此时栈是空的,那么这个字符串就是成立的,如果非空,那么就证明有多余的左括号,我们依据上述思路,写出代码
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(int i=0;i<s.size();i++)
{
if(s[i]=='('||s[i]=='{'||s[i]=='[')
{
st.push(s[i]);
}
else
{
if(st.empty()||!check(st.top(),s[i]))
return false;
st.pop();
}
}
if(!st.empty())
{
return false;
}
return true;
}
bool check(char x,char y)
{
if(x=='{'&&y=='}')
return true;
if(x=='('&&y==')')
return true;
if(x=='['&&y==']')
return true;
return false;
}
};
用队列实现栈
这道题给了我们两个队列,需要我们实现栈的功能,其实我们只需要保证一个栈为空,另一个栈逆序就可以了
其实这道题一个队列就可以实现的,我觉得用两个队列反而增加了复杂度
我们这里其实就是用一个队列,push时就将位置调好,插入数据后,将前面的数据依次重新入队列,这杨就搞出了一个,将刚入的数据放在队尾,然后依次向后排的队列
栈实现队列
设计循环队列
这道题的主要思路呢,就是预留出一个位置,当为空时front与raer都存在这里,插入数据时,都向前移动,代表有数据,直到front与rare再次相遇且不等于那个位置,为满,那个位置可以为-1,其余操作基于此生成
堆
最小的k个数
当我们第一次拿到这个题时,一看,这部很简单嘛,直接排序,输出前k个不就好了
但其实如果这样写,直接就凉凉了,因为太过于简单,虽然也是一个办法,但是谁都能想到,与其他人也没有差距
所以我们回想起了我们学习堆的时候,大堆不就有个特性,前k个不就是最小的k个嘛
树
二叉树的最大深度
这道题呢,不难,我们根据递归的思想,去分别递归左右子树,取大的而后+1,递归的深度就是我们树的深度
单值二叉树![](https://i-blog.csdnimg.cn/blog_migrate/2aaa344b32ceff517ef6ea87533f2ac6.png)
这道题同样可以使用递归,只要注意递归出口就可以
相同的树![](https://i-blog.csdnimg.cn/blog_migrate/4b22368b97744c78ca00bcfe8ca755c7.png)
这道题也可以用我们的递归简单解决
不过要注意判断的顺序
对称的二叉树
这道题呢,对于我们的传统递归而言,需要构建一个辅助函数,在辅助函数中递归,主要就在于判断左的左与右的右,左的右与右的左
另一棵树的子树![](https://i-blog.csdnimg.cn/blog_migrate/1db04b2a8264ff89e270b34b4bfb212e.png)
拿到这道题,因为我们刚才才写过两棵树相同的代码,再对左子树与右子树分别进行递归判断即可
平衡二叉树
当我们拿到这道题时,我们可以想到利用之前我们的求树的高度,而后对根进行左右递归,但是其返回值是bool类型的,所以我们递归的时候需要借助&&符号来进行