LeetCode-算法学习(必备篇)-Part3

继续来更新LeetCode的学习,本Part对应左神视频032-035。

涉及位运算,链表和数据结构的高频题目的思路讲解。原理部分较少,都是题目很多,所以把这一部分分为一个Part,即时记录思路和自己的思考以及在python实现时踩得坑。

一、位图

在C++,Java中一个int类型是32位(在python中,数位不够时编译器会自动升位),那么对于有限长度的连续数组[0,n-1],我们可以用想象有一个n长度的数组,第i位为1表示该数字i存在否则不存在,每32个长度的子数组就可以变成一个int数字,实际中我们只需要最多int(n/32)+1个数字就可以表示这样的虚拟数组,节省空间。

我们要实现位图的增删改查最关键的是确定我们要处理的数字的位置,这个数字num位于第几个数字的第几位上(下标从0开始),int(num/32)确定是第几个数字,num%32确定是第几位。核心就是这个,之后运用合理的位操作改变对应的位的值就可以了。

我们直接看具体题目吧~设计位集。这个题里面主要复杂的是翻转所有的位之后,去判断位集中每位的状态相关的函数,因为输入可能会多次翻转,翻转之后置1就变成置0了等待很麻烦。我们只用引入几个变量即可,ones 存储当前1的数量,zeros存储当前0的数量,reverse表示当前的状态是否翻转,如果翻转了reverse=true,位集中0代表存在,否则1代表存在,我们在翻转时时只用改变reverse的状态交换ones和zeros里面的数量即可。

值得注意的是,因为在统计ones和zeros的数量,所以只有在需要修改的时候进行修改,不然每次fix和unfix都会改变ones和zeros的数量。

class Bitset:

    def __init__(self, size: int):
        self.n = size
        self.size = int((size+31)/32) 
        self.arr = [int(0)]*self.size
        self.zeros = size
        self.ones = 0
        self.reverse = False
    def fix(self, idx: int) -> None:
        index = int(idx/32)
        bit = int(idx%32)
        if not self.reverse:
            if self.arr[index] &(1<<bit) == 0:
                self.arr[index] |= (1<<bit)
                self.zeros -=1
                self.ones +=1
        else:
            if self.arr[index] &(1<<bit) != 0:
                self.arr[index] &=~(1<<bit)
                self.zeros -=1
                self.ones +=1

    def unfix(self, idx: int) -> None:
        index = int(idx/32)
        bit = int(idx%32)
        if not self.reverse:
            if self.arr[index] &(1<<bit) != 0:
                self.arr[index] &=~(1<<bit)
                self.zeros +=1
                self.ones -=1
        else:
            if self.arr[index] &(1<<bit) == 0:
                self.arr[index] |= (1<<bit)
                self.zeros +=1
                self.ones -=1


    def flip(self) -> None:
        self.reverse = not self.reverse
        temp = self.zeros
        self.zeros = self.ones
        self.ones = temp


    def all(self) -> bool:
        return self.ones==self.n


    def one(self) -> bool:
        return self.ones >=1


    def count(self) -> int:
        return self.ones


    def toString(self) -> str:
        string = ''
        for i in range(self.size):
            num = self.arr[i]
            for j in range(32):
                if 32*i+j>=self.n:
                    break
                bit = (num>>j)&1
                if not self.reverse:
                    string += str(bit)
                else:
                    bit ^= 1
                    string += str(bit)
        return string

二、位运算实现加减乘除

计算机底层都是用位运算实现各种运算的,我们先在就在Cosplay计算机,所以所有的运算都不能出现加减乘除符号了,要用我们自己的算法实现。也是对leetcode上题目-两数相除的升级。

加法:加法我们可以分为两部分->无进位加法的结果+进位的结果。看个例子:a+b=001011+001110,无进位加法的结果就是a^b,进位的结果是a&b之后在向左移一位,然后我们就可以再重复这个过程,就变成了000101+010100,我们继续重复该过程知道进位是0即可。

注意!!!Python中不会处理溢出,我们左移的时候可能会溢出,记得左移都&0xffffffff将高于32位的都置0。比起这个,更需要注意的是加法的结果,因为Python是会自动升位的,也就是说如果最高位是1并不会代表是个负数而会自动变成更高位的数字去计算数值,所以返回的结果应该这样处理,其中0x7fffffff是32位最大的正数,如果比这个大说明到了负数的领域了,通过~(a^0xffffffff) 将a转化为负数,假设a=4294967295 这个数就是0xFFFFFFFF,在JAVA或者C++的interesting中应该是-1,a^0xFFFFFFFF=0,~0=-1,我们就将他变成了负数。

 a if a<=0x7fffffff else ~(a^0xffffffff) 
def add(a, b):
        while b != 0:
            ans = (a ^ b)& 0xffffffff
            b = ((a & b) << 1) & 0xffffffff
            a = ans
        return a if a<=0x7fffffff else ~(a^0xffffffff)

 

减法:很简单,a-b=a+(-b),对于一个数字取其相反数就是-b = ~b+1,但是此时我们是计算机没有+这个符号,所以-b = add(~b,1)

def neg(a):
        return add(~a, 1)
def minus(a, b):
        return add(a, self.neg(b))

乘法:我们思考一下乘法的计算过程列竖式计算的过程:对二进制来说,a=001011,b=001010,我们只用看最右侧的1在第i位,将a左移i位和a相加,之后将b右移i位,再看此时最右侧的1在第k位,我们将a左移i+k位和上一步的结果相加,直到b=0了。代码实现的更巧妙,我一直左移a右移b,如果b的最右是1就给结果加上当前的a值。

  57                001011
* 21               *001010
-----              -------
  57                000000      
114                001011       
-----             000000 
1197             001011
def multify(a, b):
        ans = 0
        while b != 0:
            if (b & 1) != 0:
                ans = add(ans, a)
            a = (a << 1) & 0xffffffff
            b = b >> 1
        return ans

 除法:首先我们处理最常见的情况之后讨论特殊情况。对于a/b,当a>=0,b>0时的情况,比如我们计算280/25,我们可以这样想280还有多少个25*2^i,280 = 25*2^3+25*2^1+25*2^0+余数(<25的部分),280/25(只考虑整数部分)=2^3+2^1+2^0。那么问题就是,从30开始向0遍历,280中含有25*2^30吗,不含有继续,直到280中含有25*2^3了,接下来讨论(280-25*2^3) 中含有25*2^2吗,依次类推直到25*2^0次方,结束循环,将含有的部分加起来即可。如何表示含有,就是280是不是>=25*2^i,此时有两种方式表达1)280>=25<<i 和2) 280>>i>=25,很明显2)好,因为不会溢出。

如果ab存在负数,也好说,我们记录下ab的符号,都变成正数做除法,最终结果根据两个数正负号调整即可。

    def div( a, b):
        x = neg(a) if a < 0 else a
        y = neg(b) if b < 0 else b
        ans = 0
        i=30
        while i>=0:
            if (x >> i) >= y:
                ans |= (1 << i)
                x = minus(x, y << i)
            i = minus(i,1)
        return neg(ans) if (a < 0) ^ (b < 0) else ans

有了负数就要讨论特殊情况,如果ab中有整数最小值MIN呢?整数最小值是没有相反数的,我们可以讨论,如果ab都是MIN是,返回1就行。如果a不是MIN,b是,则直接返回0,如果a=MIN,b=-1,返回整数最大值,最麻烦的是如果a=MIN,b就是一个常规的数字,我们将问题转化成先将a变成一个不那么小的数字,在进行计算

b>0: \frac{a+b}{b}=\frac{a}{b}+1

b<0: \frac{a-b}{b}=\frac{a}{b}-1 

def divide(dividend: int, divisor: int) -> int:
        MIN = add(1 << 31, 0)
        MAX = add(neg(1), MIN)
        if dividend == MIN and divisor == MIN:
            return 1
        if dividend != MIN and divisor != MIN:
            return div(dividend, divisor)
        if dividend == MIN and divisor == neg(1):
            return MAX
        if dividend != MIN and divisor == MIN:
            return 0
        if divisor > 0:
            dividend = add(dividend, divisor)
            ans = minus(div(dividend, divisor), 1)
        else:
            dividend = minus(dividend, divisor)
            ans = add(div(dividend, divisor), 1)
        return ans

 注意一下Pyhon中32位整数最大最小值实现即可

三、链表高频题目和必备技巧

这节课的内容思路都很好理解,就是代码写起来真的难写,很容易出bug,简单的看着思路debug一会就出来了,复杂的我现在得对着java代码看着改😂。菜就多练吧。

1.返回两个无环链表相交的第一个节点

首先明确一点,两个链表相交了最后一定会输出同一个非空节点,因为链表的next是能链接一个节点所以不可能分叉的。思路就是,我们先遍历两个链表获取其长度A,B,我们计算两个链表的长度差,之后让长的链表先next长度差步,之后短的链表也开始next,直到两个链表到同一个节点了,返回该节点。

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
        if headA == None or headB == None:
            return None
        lenA = 0
        lenB = 0
        tempA = headA
        tempB = headB
        while tempA.next !=None:
            tempA = tempA.next
            lenA+=1
        while tempB.next !=None:
            tempB = tempB.next
            lenB+=1
        
        if tempA!=tempB:
            return None
        
        diffAB = abs(lenA-lenB)

        if  lenA>=lenB:
            bigHead = headA
            smallHead = headB
        else:
            bigHead = headB
            smallHead = headA

        while diffAB!=0:
            bigHead = bigHead.next
            diffAB -=1
        
        while bigHead != smallHead:
            bigHead = bigHead.next
            smallHead = smallHead.next
        
        return smallHead

2.每k个节点一组翻转链表

如果不够k个,那么这组节点就不反转了,只会出现在最后一组。在反转的时候,比较复杂的相邻两组的尾头连接,我们举个例子:某两个相邻的段a-b-c-d-e-f,k=3,前面三个反转后变成c-b-a-d-e-f,此时第一组的尾是a第二组的头是d,但是当第二组反转后此时变成c-b-a-f-d-e,第二组的头变成f了,所以我们要记录一下第一组的尾,等第二组反转完之后重新连接第二组的新头,再更新这个变量。

因为返回的是反转后的头节点,所以第一组反转需要单独处理保留住初始的头

class Solution:
    def teamEnd(self,start,k):
        while start!=None and k>1:
            start = start.next
            k-=1
        return start
    
    def reverseNode(self,start,end):
        e = end.next
        cur = start
        pre = None
        while cur!=e:
            next = cur.next
            cur.next = pre
            pre = cur
            cur = next
        start.next = e

    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        start = head
        end = self.teamEnd(start,k)
        if end == None:
            return None
        
        result_head = end
        self.reverseNode(start,end)
        lastteamEnd = start
        while lastteamEnd.next:
            start = lastteamEnd.next
            end = self.teamEnd(start,k)
            if end == None:
                return result_head
            self.reverseNode(start,end)
            lastteamEnd.next = end
            lastteamEnd = start
        
        return result_head

3.复制带有随机指针的链表

现在节点类不止有next了还有一个random指针随机指向链表内的另一个指针也可能指空,先需要把每个节点都复制下来,包括该节点的值,next指针和随机指针。

思路:比如现在链表是a-b-c,随机指针a-c,2-1,3-空,我们现在在每个原来的节点后面复制该节点变成a-a'-b-b'-c-c',那么a-c,a'-c',而此时a'和c'的位置也是知道的了,就是a的next和c的next,我们就可以轻松的复制random指针了。再所有复制节点的random指针复制完和后,将两个链表分离即可。

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        if not head:
            return None

        cur = head
        while cur :
            next = cur.next
            newNode = Node(cur.val)
            cur.next = newNode
            newNode.next = next
            cur = next
        
        cur = head
        newcur = head.next
        while cur:
            randomcur = cur.random
            newcur.random = randomcur.next if randomcur != None else None
            cur = newcur.next
            newcur = cur.next if cur != None else None
        
        ans = head.next
        temp = ans
        cur = head
        while cur:
            next = temp.next
            cur.next = next
            ansnext = next.next if next!=None else None
            temp.next = ansnext
            cur = next
            temp = ansnext
        return ans

4.判断链表是否存在回文结构。

这个题是简单题,因为如果用栈去做就很简单,我们准备一个栈将链表每个值倒入,然后栈和链表开始同步遍历即可。但是这样空间复杂度会比较高。

用链表就不会有额外的空间复杂度。我们用快慢指针,寻找链表的中点,Slow指针每次走一步,Fast指针每次走两步,当Fast到尾的时候,Slow一定在链表的中间,不管链表是奇数长度还是偶数长度,可以自己画个图看一下,十分容易理解。之后我们我们将中点到尾部的链表反转,中点指向空,此时链表有两个头,从这两个头一起开始遍历对比,如果所有值都一样那就是回文了。

如果想优雅一点,可以判断结束之后把链表还原一下。

class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        if not head or not head.next:
            return True
        Slow = head
        Fast = head
        while Fast.next and Fast.next.next:
            Slow = Slow.next
            Fast = Fast.next.next

        pre = Slow
        cur = pre.next
        pre.next = None
        while cur:
            next = cur.next
            cur.next = pre
            pre = cur
            cur = next
        
        ans = True
        left = head
        right = pre
        while  left and  right:
            if left.val != right.val:
                ans = False
                break
            left = left.next
            right = right.next
        #链表还原
        cur = pre.next
        pre.next = None
        while cur:
            next = cur.next
            cur.next = pre
            pre = cur
            cur = next
        return ans

5.返回链表的第一个入环节点。

这个题的思路直接记住即可。思路十分简单:

准备快慢指针Slow和Fast

1)Slow一次走一步,Fast一次走两步,如果有环那么一定会相遇,走到两个指针相遇

2)此时让Fast指针回到链表的头节点,Fast也开始一次只走一步,继续和Slow指针一起往下走

3)再次相遇的位置就是第一个入环点

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head or not head.next or not head.next.next:
            return None
        s = head.next
        f = head.next.next
        while s!=f:
            if f.next==None or f.next.next==None:
                return None
            s =s.next
            f = f.next.next
        f = head

        while f!=s:
            f = f.next
            s = s.next
        
        return s

6.在链表上排序

要求时间复杂度O(n*logn),空间复杂度O(1),还得有稳定性。首先这个要求在数组上是做不到的,数组上最多只能满足其中的两个,比如归并排序,他的空间复杂度就是O(n)。但是在链表上用归并排序就可以做到,并且不能用递归,因为递归会占用系统栈额外的空间复杂度是O(logn)也不满足。总结,用非递归的方法在链表上做归并排序即可

道理我都懂了,实现就没有捷径了。非递归的归并排序就是step是1,merge->step*2再merge如此反复,在链表上其实可以参考每k个节点翻转链表那样,注意一下两组之间头尾节点的连接问题。

虽然比较复杂,但是对着代码和之前的数组上的归并排序一起理解,再画画图还是比较容易理解的,写出来还是很难的。

class Solution:
    def __init__(self):
            self.start=None
            self.end = None

    def findEnd(self,s,k):
        while s.next and k>1:
            s = s.next
            k -=1
        return s
    
    def merge(self,l1,r1,l2,r2):
        if l1.val<=l2.val:
            self.start = l1
            pre = l1
            l1 = l1.next
        else:
            self.start = l2
            pre = l2
            l2 = l2.next
        
        while l1 and l2:
            if l1.val<=l2.val:
                pre.next = l1
                pre =l1
                l1 = l1.next
            else:
                pre.next = l2
                pre =l2
                l2 = l2.next
        
        if l1:
            pre.next = l1
            self.end = r1
        else:
            pre.next = l2
            self.end = r2
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head == None:
            return None
        h = head
        size =0 
        while h:
            h = h.next
            size+=1
        
        step =1
        while step<size:
            l1 = head
            r1 = self.findEnd(l1,step)
            l2 = r1.next
            r2 = self.findEnd(l2,step)
            next = r2.next
            r1.next = None
            r2.next = None
            self.merge(l1,r1,l2,r2)
            head = self.start
            lastTeamEnd = self.end
            while next:
                l1 = next
                r1 = self.findEnd(l1,step)
                l2 = r1.next
                if l2 == None:
                    lastTeamEnd.next = l1
                    break
                r2 = self.findEnd(l2,step)
                next = r2.next
                r1.next = None
                r2.next = None
                self.merge(l1,r1,l2,r2)
                lastTeamEnd.next = self.start
                lastTeamEnd = self.end
            step*=2
        return head

四、数据结构设计高频题

先叠个甲,python中没有直接像JAVA中的hashmap和hashset,所以我的实现都是用的dict和list。因为set太难用了。。。

1.setALL功能的哈希表

setALL就是说,当调用该函数时,我将此时哈希表内所有的key都改成一个value。要求时间复杂度O(1)那就是不能遍历了。我们可以在加入数据时多加一个时间戳参数,本来key value是2:3,现在变成2:[3,timestamp],第一个的timestamp=1,第二个就是2,此时我们定义一个setallValue和setallTime记录setaALL调用时希望改变的值和调用的时间戳,后面我们再查询哈希表时,被查询的key时间戳小于setallTime时值就是setallValue,大于时就是他自己的值。

这个题我没有在牛客上做,我贴出GPT改的JAVA代码

import sys

class SetAllHashMap:
    def __init__(self):
        self.map = {}
        self.setAllValue = 0
        self.setAllTime = -1
        self.cnt = 0

    def put(self, k, v):
        if k in self.map:
            self.map[k] = [v, self.cnt]
        else:
            self.map[k] = [v, self.cnt]
        self.cnt += 1

    def setAll(self, v):
        self.setAllValue = v
        self.setAllTime = self.cnt
        self.cnt += 1

    def get(self, k):
        if k not in self.map:
            return -1
        value = self.map[k]
        if value[1] > self.setAllTime:
            return value[0]
        else:
            return self.setAllValue

def main():
    # 输入读取
    input = sys.stdin.read
    data = input().split()
    
    set_all_hashmap = SetAllHashMap()
    idx = 0
    n = int(data[idx])
    idx += 1
    
    for _ in range(n):
        op = int(data[idx])
        idx += 1
        if op == 1:
            a = int(data[idx])
            idx += 1
            b = int(data[idx])
            idx += 1
            set_all_hashmap.put(a, b)
        elif op == 2:
            a = int(data[idx])
            idx += 1
            print(set_all_hashmap.get(a))
        else:
            a = int(data[idx])
            idx += 1
            set_all_hashmap.setAll(a)

if __name__ == "__main__":
    main()

2.实现LRU结构。

LRU结构就是假设是个数组,这个数组最多只能放k个数,当要弹出时,弹出最久没有过操作的数,如果有查询,修改都要更新被操作数的操作时间。比如现在按照操作时间排序位a,b,c,此时a十最久没有被操作的,c是最近被操作的,数组最多放3个数,此时a被访问了就变成,b,c,a,要加入d了,弹出b,把d放到最后变成c,a,d。

我们用hashmap和双向链表实现。链表的头代表最久没被操作的,尾代表最近被操作的。hashmap中key就是key值,value是将key,value打包成一个双向节点。也就是我在hashmap中索引key得到是节点,节点中也包含key,value的信息。题目要求我们实现put和get功能。我们需要什么函数实现这些功能。

首先要定义一个节点类,put操作时如果key不存在,如果LRU还有地方,就放到链表的尾部,如果没有需要将头部弹出,再放到尾部;如果put时key存在,就改变该key的value。get函数不会改变值,只是读取,但是get和put都会改变该节点的访问时间,操作之后,该节点应该到链表的尾部。链表需要尾部加节点、移除头节点功能和将任意节点移动到尾部的功能,注意要同步更新hashmap。

class Node:
    def __init__(self,key,val,next=None,last = None):
        self.val = val
        self.key = key
        self.next = next
        self.last = last

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.head = None
        self.tail = None
        self.map = {}

    def get(self, key: int) -> int:
        if key in self.map:
            ans = self.map[key]
            self.node2tail(ans)
            val = ans.val
            return val
        return -1

    def put(self, key: int, value: int) -> None:
        if key in self.map:
            node = self.map[key]
            node.val = value
            self.node2tail(node)
        else:
            if len(self.map) == self.capacity:
                temp = self.removehead()
                del self.map[temp.key]
            node = Node(key,value)
            self.map[key] = node
            self.add2tail(node)
    
    def node2tail(self,node):
        if node ==self.tail:
            return 
        if self.head == node:
            self.head = self.head.next
            self.head.last = None
        else:
            node.last.next = node.next
            node.next.last = node.last
        
        self.tail.next = node
        node.last = self.tail
        node.next = None
        self.tail = node
    
    def removehead(self):
        if self.head == None:
            return None
        ans = self.head
        if self.head == self.tail:
            self.head = None
            self.tail = None
        else:
            self.head = self.head.next
            self.head.last = None
            ans.next = None
        return ans
    
    def add2tail(self,node):
        if node ==None:
            return
        if self.head==None:
            self.head = node
            self.tail = node
        else:
            self.tail.next = node
            node.last = self.tail
            self.tail = node

3.O(1) 时间插入、删除和获取随机元素(不存在重复元素)

除了加入删除,该题目要求了一个getrandom函数随机返回结构中的一个值。

我们用hashmap和数组实现。数组很好实现插入和随机获取,但是删除不太容易,因为数组是一个连续结构,如果将某一个位置移去,该位置会空出来,否则就得遍历之后的数据一个一个往前填,就不是O(1)了,此时hashmap出场了。hashmap中的key是该数值,value是该数值的索引,当我们移除某个数时,我们可以快速查询到该数的索引,数组的最后一个数字替换该数字,之后将最后一个数字的索引变成被移除数字的索引更新hashmap即可。

import random
class RandomizedSet:

    def __init__(self):
        self.map = {}
        self.arr = []

    def insert(self, val: int) -> bool:
        if val in self.map:
            return False
        self.arr.append(val)
        self.map[val] = len(self.arr)-1
        return True

    def remove(self, val: int) -> bool:
        if val in self.map:
            i = self.map[val]
            self.arr[i] = self.arr[-1]
            self.map[self.arr[i]] = i
            del self.map[val]
            del self.arr[-1]
            return True
        return False

    def getRandom(self) -> int:
        i = random.randint(0,len(self.arr)-1)
        return self.arr[i]

4.O(1) 时间插入、删除和获取随机元素 - 允许重复

题目三的升级版,允许重复了,并且remove时有多个只移除一个即可。还是用题目三的思路,我们再hashmap中key还是数字,但是value变成一个数组,存储所有时间加入到进来的下标。我们移除的时候只用去索引到这个value中随便拿出一个索引,再像题目三那样,用数组最后一个数字覆盖该索引的值,再更新hashmap,如果此时该key删完了,记得删除map中的信息。

import random
class RandomizedCollection:

    def __init__(self):
        self.map = {}
        self.arr = []

    def insert(self, val: int) -> bool:
        if val in self.map:
            self.arr.append(val)
            val_list = self.map[val]
            val_list.append(len(self.arr)-1)
            return False
        else:
            self.arr.append(val)
            self.map[val] =[len(self.arr)-1]
            return True
    
    def remove(self, val: int) -> bool:
        if val in self.map:
            val_list = self.map[val]
            index = val_list[-1]
            val_list.pop()
            if len(val_list) ==0 :
                del self.map[val]
            if index == len(self.arr)-1:
                self.arr.pop()
            else:
                end_value = self.arr[-1]
                self.arr[index] = end_value
                self.arr.pop()
                end_value_list = self.map[end_value]
                for i in range(len(end_value_list)):
                    if end_value_list[i] == len(self.arr):
                        end_value_list[i] = index
                        break
            return True
        return False

    def getRandom(self) -> int:
        index  = random.randint(0,len(self.arr)-1)
        return self.arr[index]

5. 数据流的中位数

数据流在流入时会随时调用查询中位数的函数,不可能加一个数字从头到尾排序一次,加一个排序一次复杂度太高。我们用两个堆实现,一个大根堆一个小根堆,我们只用保证这两个堆的顶部可以包含了中位数信息即可,数据流长度是奇数时保证其中一个堆顶就是中位数,是偶数时堆顶和除2就是中位数。

第一个数进来时先进大根堆,之后,新数字比大根堆堆顶小就去大根堆,比大根堆顶顶大就去小根堆,很好理解大根堆顶是这个堆中最大的,但是比大根堆顶大的数又去了小根堆,那么最后的结果一定是小根堆顶是比大根堆顶大的数中最小的,不就是中位数信息嘛。之后,当两个内数据量插值来到2时,多的给少的弹出一个。最后就是在两个堆顶操作算中位数,两个堆数量一样那就是加起来除以二,不一样那就是多的那个堆的堆顶是中位数。

值得注意的是python中的heapq默认是实现小根堆并且没办法用比较器改变这个结构,那么我们在实现大根堆时对压入的数字取负数即可,弹出的时候再把负号去掉就行了。记得数字入大根堆时也要加负号比较。

import heapq
class MedianFinder:
    def __init__(self):
        self.bigHeap = []
        self.smallHeap = []

    def addNum(self, num: int) -> None:
        if len(self.bigHeap)==0:
            heapq.heappush(self.bigHeap,-num)
        else:
            if num <= -self.bigHeap[0]:
                heapq.heappush(self.bigHeap,-num)
            else:
                heapq.heappush(self.smallHeap,num)
            self.balance()


    def findMedian(self) -> float:
        if len(self.bigHeap) == len(self.smallHeap):
            return (-self.bigHeap[0]+self.smallHeap[0])/2
        if len(self.bigHeap) > len(self.smallHeap):
            return -self.bigHeap[0]
        else:
            return self.smallHeap[0]

    def balance(self):
        if len(self.bigHeap) - len(self.smallHeap) == 2:
            heapq.heappush(self.smallHeap,-heapq.heappop(self.bigHeap))
        if len(self.bigHeap) - len(self.smallHeap) == -2:
            heapq.heappush(self.bigHeap,-heapq.heappop(self.smallHeap))

6.最大频率栈

删除并返回堆栈中出现频率最高的元素。如果出现频率最高的元素不只一个,则移除并返回最接近栈顶的元素。主要是这一条如何实现。

我们构建两个map,第一个map很简单key是数值,value是频率,压入几次这个key的value就是几。关键是第二个map,第二个map的key是频率,value是满足这个频率的有哪些数值,因为是不断加入的所以该value的最后一个元素必然是离栈顶最近的。比如我们依次加入a,a,b,c,b,a,第一个map很好更新a:1->a:2->a:2,b:1->a:2,b:1,c:1->a:2,b:2,c:1->a:3,b:2,c:1,第二个map的更新比较关键1:[a]->1:[a],2[a]->1:[a,b],2:[a]->1:[a,b,c],2:[a]->1:[a,b,c],2:[a,b]->1:[a,b,c],2:[a,b],3:[a],同时会有一个变量记录当前最大的频率是多少。当我们要弹出时,直接去第二个map寻找最大频率对应的value弹出最后一个元素,根据弹出值再去修改第一个map中的频率值,此时如果第二个map该频率对应的value没空则不用更新最大频率,否则减1。在弹出与加入时,关注map中元素的增加与删除即可。

class FreqStack:
    def __init__(self):
        self.map={}
        self.map_arr={}
        self.max_cnt = 0

    def push(self, val: int) -> None:
        if val not in self.map:
            self.map[val] = 1
            if len(self.map_arr) == 0 :
                self.map_arr[1] = [val]
                self.max_cnt += 1
            else:
                self.map_arr[1].append(val)
        else:
            self.map[val] +=1
            if self.map[val] > self.max_cnt:
                self.max_cnt = self.map[val]
                self.map_arr[self.max_cnt] = [val]
            else:
                self.map_arr[self.map[val]].append(val)


    def pop(self) -> int:
        ans = self.map_arr[self.max_cnt].pop()
        self.map[ans] -=1
        if len(self.map_arr[self.max_cnt])==0:
            del self.map_arr[self.max_cnt]
            self.max_cnt-=1
        if self.map[ans] == 0:
            del self.map[ans]
        return ans

7.全 O(1) 的数据结构

本题用hashmap+双向链表实现。思路十分巧妙。

首先我们定义一个map,该map的key是输入的字符串,value是一个自定义的双向链表的节点。该节点除了last和next指针,存有频率和该频率有的所有字符串,类似上一个题中的第二个map做的工作。此时我们可以在map中通过key查询到节点,该节点的val可以查询到该key出现的频率。我们可以发现map中多个key如果频率一样其实对应的是一个节点。

在初始化时,我们先定义两个节点作为整个双向链表的头尾并且永远存在。头节点是频率是0,含有字符串为空,尾节点是频率是整数最大值,含有的字符串也是空。之后,又新的key进来时,先map中查询,如果不存在,我们去链表中看头节点的下一个节点是不是频率为1的节点,如果是,把这个可以加入到该节点,在map中加入该key value对,如果不是我们就新新建一个频率为1的节插入到头节点之后,再更新map;如果map中有这个key,我们就可以查到该key对应的节点,之后,我们看该节点的下一个节点的频率是不是该节点的频率+1,如果是就加入该节点,如果不是就新建,然后插入到该节点之后,同时更新map,并且删除当前节点的字符串数组中删除该key,如果空了就删除这个节点。弹出字符串同理,基本就是反过来就行。拿最大频率字符串就看链表尾节点的上一个节点中的字符串数组,最小频率字符就是看头节点下一个节点中的字符串数组,都是随机拿一个出来返回就行,不用删除。

class Bucket:
    def __init__(self,string,cnt,last=None,next=None):
        self.cnt = cnt
        self.last = last
        self.next = next
        self.arr = [string]

class AllOne:

    def __init__(self):
        self.head = Bucket("",0)
        self.tail = Bucket("",1000000)
        self.head.next = self.tail
        self.tail.last = self.head
        self.map = {}
    def inc(self, key: str) -> None:
        if key not in self.map:
            if self.head.next.cnt ==1:
                self.map[key] = self.head.next
                self.head.next.arr.append(key)
            else:
                newBucket = Bucket(key,1)
                self.map[key] = newBucket
                self.insert(self.head,newBucket)
        else:
            bucket=self.map[key]
            if bucket.next.cnt == bucket.cnt+1:
                self.map[key] = bucket.next
                bucket.next.arr.append(key)
            else:
                newBucket = Bucket(key,bucket.cnt+1)
                self.map[key] = newBucket
                self.insert(bucket,newBucket)
            bucket.arr.remove(key)
            if len(bucket.arr)==0:
                self.remove(bucket)


    def dec(self, key: str) -> None:
        bucket = self.map[key]
        if bucket.cnt ==1:
            del self.map[key]
        else:
            if bucket.last.cnt == bucket.cnt-1:
                self.map[key] = bucket.last
                bucket.last.arr.append(key)
            else:
                newBucket = Bucket(key,bucket.cnt-1)
                self.map[key] = newBucket
                self.insert(bucket.last,newBucket)
        bucket.arr.remove(key)
        if len(bucket.arr)==0:
            self.remove(bucket)


    def getMaxKey(self) -> str:
        return self.tail.last.arr[-1]


    def getMinKey(self) -> str:
        return self.head.next.arr[-1]

    def insert(self,cur,pos):
        cur.next.last = pos
        pos.next = cur.next
        pos.last = cur
        cur.next = pos
    def remove(self,cur):
        cur.last.next = cur.next
        cur.next.last = cur.last

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值