LeetCode-算法学习(入门篇)

开一个新坑,学习数据结构与算法,也是为了明年秋招做储备(非科班表示很慌QAQ)

跟随B站左神从头开始学习,目前看完了入门篇,写写笔记和自我理解,是一个备忘录的同时也是归纳总结。如有理解不对的地方也请各位大佬赐教~

一、二进制与位运算

1.以整数为例,对于n位无符号整数,其大小范围为0~2^{n}-1,对于有符号整数,其第一位是符号位则大小范围是-2^{n-1}~2^{n-1}-1,个数都是2^{n}个。

2.计算机中如何对一个正数取相反数?b = (~a)+1与b=-a在计算机中是一个意思,但是在计算机中-2^{n-1}是无法取相反数的,因为最大的正数是2^{n-1}-1,其相反数仍是自己

3.位运算,|,&,^(异或,相同为0不同为1),~,<<,>>,>>>。>>是右移后左边用符号位补位,>>>是右移后左边用0补位,所以对于非负数两个符号没有区别,对于负数有区别

4.二进制打印函数中用移位和与操作实现,对每一位用代码a判断,而不能用代码b。假如与操作第3位的结果是0b1000,那么此时该数不等于1而等于8,用b代码判断结果就是0,而a代码判断结果就是1。

a) num&(1<<i)==0?'0':'1'   b) num&(1<<i)==1?'1':'0'

5.计算机不会判断是否会溢出,在进行运算时需要自己保证运算的结果不会溢出,包括中间变量,a,b代码的理论运算结果是一样的,但是a+b可能会存在溢出,所以b)代码更好

a) m=(a+b)/2                   b) m= b + (a-b)/2

6. 为什么在二进制中负数设计的这么反人类,是为了加法逻辑是一套逻辑没有条件转移,计算机底层只有加法运算。

比如 3+1->0011+0001=0100=4, -5+7->1011+0111=0010=2

二、选择、冒泡、插入排序

1.选择排序:i~n-1范围上,找到最小值并放在i位置上,然后i+1~n-1范围上继续

def SelectSort(arr):
    length = len(arr)
    for i in range(length):
        mindex = i
        for j in range(i + 1, length):
            if arr[j] < arr[mindex]:
                mindex = j
        swap(arr, mindex, i)
    return arr

2.冒泡排序:0~i范围上,相邻位置较大的数滚下去,最大值最终来到i位置,然后0~i-1范围上继续

def BubbleSort(arr):
    length = len(arr)
    for i in range(length - 1, -1, -1):
        for j in range(i):
            if arr[j] > arr[j + 1]:
                swap(arr, j, j + 1)
    return arr

3.插入排序:0~i范围上已经有序,新来的数从右到左滑到不再小的位置插入,然后继续

def InsertSort(arr):
    length = len(arr)
    for i in range(1, length):
        for j in range(i - 1, -1, -1):
            if arr[j] > arr[j + 1]:
                swap(arr, j, j + 1)
    return arr

其中swap函数是交换数组中两个数字位置

def swap(arr, i, j):
    temp = arr[i]
    arr[i] = arr[j]
    arr[j] = temp

只能说概念好理解,代码还真不一定能一次写对(⊙﹏⊙),常看常新吧

三、对数器

这个感觉就有点难了,自己想出两种不同的思路去解题。在leetcode中的题目我们可以通过提交看报错一步一步改,但是现实中面经啊,或者别人说的面试题就没这个条件了,所有我们需要想一个暴力解,能解出来但是毫无美感,另一个是期望的最优解,然后不停的生成随机输入测试我们的最优解,和暴力解的答案对比,不断修改最优解,直到两个方法的解完全一样,此时最优解就优化完成。前提是暴力解得保证是对的。

四、二分搜索

有序数组中确定num是否存在。二分搜索的思路很简单,找数组中间的那个数,num比这个数大那就往右边找,否则往左边找,每次都可以在将搜索范围减半,时间复杂度就只有O(logN)

对于非有序数组(任何两个相邻的值不相等)也可以做一些工作,比如寻找峰值问题,假设数组num中num[-1]=num[n]=无穷小,类似于高数中的极小值和极大值一样,比如num[1]>num[0],num[n-2]>num[n-1]那么中间一定有至少一个峰值。

思路就是,先判断0或者n-1位是不是峰值,如果不是,二分数组看中间的数是不是峰值,否则对左侧或者右侧进行二分,以此循环。

def findPeakElement(self, nums: List[int]) -> int:
        length = len(nums)
        if length == 1:
            return 0
        if nums[0] > nums [1]:
            return 0
        if nums[length-1] > nums[length -2]:
            return length-1
        l = 1
        r = length -2
        while l<=r:
            mid = l + ((r-l)>>1)
            if nums[mid-1]<nums[mid] and nums[mid] > nums[mid+1]:
                return mid
            else:
                if nums[mid-1] >nums[mid]:
                    r = mid -1
                elif nums[mid+1]>nums[mid]:
                    l = mid +1

五、时间复杂度和空间复杂度

建议直接看视频,后续再补充。。。

六、算法和数据结构简介

左神将算法分为了硬计算类算法、软计算类算法。

硬计算类算法:精确求解。但是某些问题使用硬计算类的算法,可能会让计算的复杂度较高。大厂算法和数据结构笔试、面试题、acm比赛或者和acm形式类似的比赛,考察的都是硬计算类算法。软计算类算法:更注重逼近解决问题,而不是精确求解。计算时间可控比如:模糊逻辑、神经网络、进化计算、概率理论、混沌理论、支持向量机、群体智能

硬计算类算法是所有程序员岗位都会考、任何写代码的工作都会用到的。前端、后端、架构、算法所有岗位都要用到。但是算法工程师除了掌握硬计算类的算法之外,还需要掌握软计算类的算法。

学就完事了!

七、单双链表及其反转-堆栈诠释

1)按值传递与按引用传递。在C++中应该很有体会,f(a)与f(&a)的区别,前者并不会改变输入a的值,后者会改变输入a的值。计算机中只有访问该变量内存中的地址并修改值才会改变该值,否则就是新创建一个变量,修改创建变量的值。

2)单双链表,就是链表节点有没有存储上一个节点的地址。

3)反转链表:单链表反转由1->2变成1<-2

def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        next = None
        pre = None
        while head!=None:
            next = head.next
            head.next = pre
            pre = head
            head = next
        return pre

八、链表入门题目-合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。

思路:先判断是否有空链表,如果没有先判断哪个链表的头小,小的为新链表的头,注意链表的头后续不再改变。通过cur1与cur2索引后续的节点,选择较小的与上一个节点pre连接,最后当一个链表遍历完之后,将另一个链表的剩余部分全部接上。

def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        if list1==None or list2==None:
            if list1==None:
                return list2
            else:
                return list1
        
        if list1.val<=list2.val:
            head = list1
            cur1 = head.next
            cur2 = list2
        else:
            head = list2
            cur1 = head.next
            cur2 = list1
        
        pre = head

        while cur1!=None and cur2!=None:
            if cur1.val <=cur2.val:
                pre.next = cur1
                cur1 = cur1.next
            else:
                pre.next = cur2
                cur2 = cur2.next
            pre = pre.next
        
        if cur1==None:
            pre.next = cur2
        else:
            pre.next = cur1
        
        return head

九、链表入门题目-两个链表相加

首先该问题不能将链表数字转换成int/long类型相加完之后再写成链表,因为有可能溢出。链表是可以无限长的,但是整数是有位数限制的。所以老老实实按照链表做。

链表中头节点是个位,按位相加时要考虑两个问题,第一对应位上是否有数字可加,第二进位问题。无论该位有没有数字可以相加都要考虑前一位的进位,三位数+四位数可以等于五位数。对着代码还是很容易理解的。

def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        ans = None
        pre = None
        cur1 = l1
        cur2 = l2

        carry = 0
        while cur1!= None or cur2 != None:
            sum = 0
            if cur1 == None:
                sum += cur2.val+ carry
                cur2 = cur2.next
            elif cur2== None:
                sum += cur1.val+ carry
                cur1 = cur1.next
            else:
                sum = cur1.val + cur2.val + carry
                cur1 = cur1.next
                cur2 = cur2.next
            value = sum%10
            carry = int(sum/10)

            if ans==None:
                ans = ListNode(value)
                pre = ans
            else:
                temp = ListNode(value)
                pre.next = temp
                pre = pre.next

        
        if carry == 1:
            temp = ListNode(1)
            pre.next = temp
        return ans

十、链表入门题目-划分链表

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。你应当 保留 两个分区中每个节点的初始相对位置。

将链表分成两个区域,<x与>=x,在这两个区域中,节点的相对位置不变。

思路:遍历链表之后尾插。我们可以假设有两个链表一个是<x一个是>=x,每个链表有头尾节点,从头遍历输入的链表,按照要求尾插入假设的两个链表,最后将小链表的尾与大链表的头相连即可。

def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
        bigHead = None
        bigTail = None
        smallHead = None
        smallTail = None
        while head !=None:
            next = head.next
            head.next = None
            if head.val < x:
                if smallHead==None:
                    smallHead = head
                else:
                    smallTail.next = head
                smallTail = head
            if head.val >= x:
                if bigHead==None:
                    bigHead = head
                else:
                    bigTail.next = head
                bigTail = head
            head = next
        if smallHead == None:
            return bigHead
        else:
            smallTail.next = bigHead
            return smallHead

十一、队列和栈-链表、数组实现

1.队列,先进先出,尾部加入头部弹出。队列的链表实现比较简单,数据加入只能加入到链表的尾部,弹出只能弹出头部,再将链表的头向后移一个。队列的数组实现,用l和r指示数组的最左边和最右边,l<r有数,l==r就是空,加入就是放到数组r位置r++,弹出就是取出数组l位置,l++。

2.栈,先进后出,尾部加入尾部弹出。栈的数组实现,用size表示数组大小,size==0数组栈为空,加入就是放到数组的size位置,size++,弹出就是弹出size-1位置,size--。

3.环形队列的数组实现。环形队列就是头尾的位置(l/r)可以动态变化,用size管理头尾,队列的大小不能超过n。当size<n时,加入数据放到r处,r++,size++,当r==n-1时,加入数据后r=0,当size>0时,弹出l处的数据,l++,size--,当l==n-1时,弹出数据后l=0。

class MyCircularQueue:

    def __init__(self, k: int):
        self.arr = [None for _ in range(k)]
        self.l = 0
        self.r = 0
        self.k=k
        self.size = 0

    def enQueue(self, value: int) -> bool:
        if self.isFull():
            return False
        self.arr[self.r] = value
        if self.r == self.k-1:
            self.r=0
        else:
            self.r+=1
        self.size+=1
        return True

    def deQueue(self) -> bool:
        if self.isEmpty():
            return False
        if self.l==self.k-1:
            self.l = 0
        else:
            self.l +=1
        self.size-=1
        return True

    def Front(self) -> int:
        if self.isEmpty():
            return -1
        return self.arr[self.l]

    def Rear(self) -> int:
        if self.isEmpty():
            return -1
        if self.r==0:
            return self.arr[self.k-1]
        else:
            return self.arr[self.r-1]

    def isEmpty(self) -> bool:
        return self.size ==0


    def isFull(self) -> bool:
        return self.size == self.k

十二、队列和栈入门题目-栈和队列相互实现

1.用栈实现队列:我们用两个栈实现队列,因为栈是先进后出,那么将in栈的数据倒入到out栈中,则可以实现顺序颠倒,进而实现先进先出。首先所有的数据都是先进入in栈,之后在倒入到out栈中必须保证两点:(1)out栈必须是空的;(2)in栈必须全部倒入out栈。这两点保证out栈弹出的顺序不会改变。所有数据的都是in栈进入,out栈弹出。

class MyQueue:

    def __init__(self):
        self.in_stack = []
        self.out_stack = []


    def push(self, x: int) -> None:
        self.in_stack.append(x)
        self.in2out()

    def pop(self) -> int:
        if self.empty():
            return None
        self.in2out()
        temp = self.out_stack.pop()
        return temp

    def peek(self) -> int:
        if self.empty():
            return None
        self.in2out()
        temp = self.out_stack[-1]
        return temp

    def empty(self) -> bool:
        if len(self.in_stack) + len(self.out_stack)==0:
            return True
        else:
            return False

    def in2out(self):
        if len(self.out_stack)==0 and len(self.in_stack)!=0:
            for i in range(len(self.in_stack)):
                self.out_stack.append(self.in_stack.pop())

2.用队列实现栈。题目要求用两个队列实现,左神的思路用一个队列即可,并且十分巧妙。核心为:后来的数据把队列中现有的数据一个一个挤出去再重新入队,这样就可以保证新进来的数在队列的头部,可以从头部顺利弹出。(队列只能尾进头出)队列中有几个数字该过程就重复几次。

class MyStack:

    def __init__(self):
        self.list=[]


    def push(self, x: int) -> None:
        length = len(self.list)
        self.list.append(x)
        for i in range(length):
            temp = self.list.pop(0)
            self.list.append(temp)


    def pop(self) -> int:
        if self.empty():
            return None
        return self.list.pop(0)


    def top(self) -> int:
        if self.empty():
            return None
        return self.list[0]


    def empty(self) -> bool:
        if len(self.list)==0:
            return True
        else:
            return False

十三、栈的入门题目-最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。该题的核心是在栈进行push和pop操作后仍可以快速检索到最小元素。最开始想的是在压入的时候记录最小值,但是如果最小值弹出了就无法更新了。左神的思路是:用两个栈实现,一个是data栈存储压入的数据,一个是min栈记录压入第i个元素时最小值,min栈第一个元素和data栈一样,从第二个开始进行比较,如果压入的元素比当前min栈元素小则压入,否则压如当前min栈的元素,在弹出数值时两个栈同时弹出。所以在检索最小元素时只需要检索min栈顶元素即可。

class MinStack:

    def __init__(self):
        self.data=[]
        self.min=[]


    def push(self, val: int) -> None:
        self.data.append(val)
        if len(self.min) ==0 or val<= self.min[-1]:
            self.min.append(val)
        else:
            self.min.append(self.min[-1])


    def pop(self) -> None:
        self.min.pop()
        self.data.pop()


    def top(self) -> int:
        return self.data[-1]


    def getMin(self) -> int:
        return self.min[-1]

十四、双端队列-双链表和固定数组实现

双端队列就是头部和尾部都可以进出数据,如果用双链表实现,注意头尾的指针和新数据一段指向空即可。主要讨论固定数组实现。

固定数组实现时采用循环队列的思路管理数组大小,用size管理数组现在的大小,因为时固定数组所以size是有最大值k的。我们定义头部指针为l,尾部为r。总结进入队列和弹出队列的逻辑为:

头部进入:l==0时,数据进入k-1位置,l=k-1;l!=0时,数据进入l-1位置,l-=1

头部弹出:弹出[l]位置数据,l=k-1时,令l=0;l!=k-1时,令l+=1

尾部进入:r=k-1时,数据进入0位置,r=0;r!=k-1时,数据进入r+1位置,r+=1

尾部弹出:弹出[r]位置数据,r=0时,令r=k-1;r!=0时,令r-=1

class MyCircularDeque:

    def __init__(self, k: int):
         self.limit = k
         self.l=self.r=self.size=0
         self.data=[-1]*8001


    def insertFront(self, value: int) -> bool:
        if self.isFull():
            return False
        if self.isEmpty():
            self.l=self.r=0
            self.data[0]=value
        else:
            if self.l==0:
                self.data[self.limit-1] = value
                self.l = self.limit-1
            else:
                self.l -=1
                self.data[self.l] = value
        self.size+=1
        return True

    def insertLast(self, value: int) -> bool:
        if self.isFull():
            return False
        if self.isEmpty():
            self.l=self.r=0
            self.data[0]=value
        else:
            if self.r==self.limit-1:
                self.data[0] = value
                self.r = 0
            else:
                self.r +=1
                self.data[self.r] = value
        self.size+=1
        return True

    def deleteFront(self) -> bool:
        if self.isEmpty():
            return False
        if self.l == self.limit-1:
            self.l=0
        else:
            self.l+=1
        self.size-=1
        return True

    def deleteLast(self) -> bool:
        if self.isEmpty():
            return False
        if self.r == 0:
            self.r = self.limit-1
        else:
            self.r-=1
        self.size-=1
        return True

    def getFront(self) -> int:
        if self.isEmpty():
            return -1
        return self.data[self.l]


    def getRear(self) -> int:
        if self.isEmpty():
            return -1
        return self.data[self.r]


    def isEmpty(self) -> bool:
        return self.size==0


    def isFull(self) -> bool:
        return self.size==self.limit

十五、二叉树及其三种序的递归实现

二叉树有三种遍历方式先序(中左右)中序(左中右)后序(左右中)。使用递归算法实现时,时间复杂度为O(n),空间复杂度为O(h),h为树的高度。

我们以下图的树为例: 对于遍历的原则是对于任何一颗子树都要满足该种遍历的顺序

先序:1,2,4,5,3,6,7。中序:4,2,5,1,6,3,7。后序:4,5,2,6,7,3,1。

用递归的算法写十分简单,在不同位置拿数据即可实现不同的遍历方式。

class Solution:
    def __init__(self):
        self.result=[]
    def anyorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if root==None:
            return []
        # 先序遍历则在此压入结果队列
        #self.result.append(root.val)
        self.anyorderTraversal(root.left)

        # 中序遍历则在此压入结果队列
        #self.result.append(root.val)
        self.anyorderTraversal(root.right)

        # 后序遍历则在此压入结果队列
        self.result.append(root.val)

        return self.result

那么我们该如何理解这个递归呢?递归的终止条件是该节点没有左右叶子节点了开始往上一级回溯,以之前的图为例,整个递归序列是1,2,4,4,4,2,5,5,5,2,1,3,6,6,6,3,7,7,7,3,1。每个空节点只会来一次,就return了,回溯到上一个节点了,但是每个非空节点会遍历3次,那么我们的先,中,后序就是第几次遍历该非空节点时弹出数据。第一次遇到该节点就弹出就是先序,第二次中序,第三次后续。从递归序的角度更好理解该递归代码和深层逻辑。

十六、二叉树遍历的非递归实现

递归实现的代码非常简单逻辑有点绕,但是非递归的我感觉代码也不简单逻辑也不简单QAQ。左神主要推荐用一个栈实现二叉树的不同遍历方式。

1.先序遍历:因为栈是先进后出,所有要实现中左右的顺序,对于左右节点应该先压入右节点数据再压入左节点数据。大概逻辑就是,先压入根节点到栈,此时栈内有数据,当栈不空时,弹出顶层节点之后先压右节点再压左节点,直到栈空了就是遍历完了。理解就是,在压入左右节点之前栈顶一定是该子树的根节点,如果已经到叶子节点,那么再下一个循环里就会被弹出。并且一定是先弹出上一个根节点的左节点。

def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        stack = []
        result = []
        if root==None:
            return []
        stack.append(root)
        while len(stack) != 0:
            head = stack.pop()
            result.append(head.val)
            if head.right!=None:
                stack.append(head.right)
            if head.left!=None:
                stack.append(head.left)
            
        return result

2.中序遍历:中序的是左中右,对于任何一个节点都应该先处理左树再处理右树。当栈不空或者该节点还有子树时,先将该子树的左边全部压入栈,之后弹出栈顶元素,再将该弹出节点的右边全部压入,如此循环即可。以上图为例,首先压入1,2,4此时弹出4,4没有右节点继续弹出2,2有右节点压入5,5没有右节点了,弹出5,此时栈里只剩1,弹出,将3,6压入,弹出6,弹出3,压入7,弹出7,此时栈空并且该节点也没有子树了结束,顺序就是4,2,5,1,6,3,7。

def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        stack = []
        result = []
        if root==None:
            return result
        while len(stack)!=0 or root!=None:
            if root!= None:
                stack.append(root)
                root = root.left
            else:
                root = stack.pop()
                result.append(root.val)
                root = root.right
        return result

3. 后序遍历:如果我们用两个栈实现,可以这样。我们前序遍历时中左右,控制左右是先压右再压左,如果我们颠倒这个顺序就可以实现中右左,再将这个结果倒入第二栈实现反转就是左右中的后序遍历。

主要还是看一个栈如何实现,我们引入一个新的变量h,h是记录上次弹出的节点位置的变量。当还没有弹出时,h一直在树的根节点,弹出节点x之后,h就到了x的位置。我们主要判断就是该节点左树是否存在并且左树是否被处理之后判断右树是否存在且右树是否被处理。感觉有点绕😵。我们继续用之前的例子来理解,最开始h在1位置,将根节点压入栈,此时1有左树并且没有被处理,我们将2,4依次压入,此时4节点没有左右节点了,弹出4,h到4的位置,此时栈顶时2节点,那么此时2节点的左树已经处理完了,该处理右树了,将5压入,5没有左右节点弹出,h到5,此时2节点的左右节点处理完了,弹出2,h到2,此时栈内剩1节点,1节点的左边处理完了但是右边还没有处理,压入3,6,同理弹出6,h到6的位置,压入7,弹出7,h到7的位置,弹出3,h到3的位置,最后弹出1,栈空结束循环。还是得对着图和代码理一理

def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        stack = []
        result = []
        if root == None:
            return result
        stack.append(root)
        while len(stack)!=0:
            cur = stack[-1]
            if cur.left != None and root != cur.left and root != cur.right:
                stack.append(cur.left)
            elif cur.right!=None and root!=cur.right:
                stack.append(cur.right)
            else:
                result.append(cur.val)
                root=stack.pop()
        return result

总结

入门部分确实十分基础,我这种小白也可以顺利的跟下来,但是还是要多想代码的逻辑和多敲代码,从理解到能自己完全写下来之间也是有巨大鸿沟的。

Talk is cheap. Show me the code.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值