leetcode--基础储备题

本章节会依次收录在学习过程中遇到的经典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,递归的深度就是我们树的深度

 单值二叉树

 这道题同样可以使用递归,只要注意递归出口就可以

 相同的树

 这道题也可以用我们的递归简单解决

 

 不过要注意判断的顺序

对称的二叉树

 这道题呢,对于我们的传统递归而言,需要构建一个辅助函数,在辅助函数中递归,主要就在于判断左的左与右的右,左的右与右的左

 另一棵树的子树

 拿到这道题,因为我们刚才才写过两棵树相同的代码,再对左子树与右子树分别进行递归判断即可

 平衡二叉树

 当我们拿到这道题时,我们可以想到利用之前我们的求树的高度,而后对根进行左右递归,但是其返回值是bool类型的,所以我们递归的时候需要借助&&符号来进行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值