[Leetcode] self designed data structure of data structure -- python version

Posted by sintax63 on Wed, 05 Jan 2022 11:29:59 +0100

This article summarizes the common algorithms of designing data structure according to labuladong's algorithm sketch, which is implemented in Python 3

LRU algorithm

LRU(Least Recently Used) algorithm is a cache elimination mechanism. The computer's cache capacity is limited. If the cache is full, some contents will be deleted. What contents will be deleted? LRU believes that the recently used data is useful, and the data that has not been used for a long time is useless. When the memory is full, priority will be given to deleting the data that has not been used for a long time.

LRU caching mechanism, T146

Title: design and implement a data structure that meets LRU cache constraints.

Requirement: put and get time complexity is O(1)

Solution: hash linked list

  1. Use python's own hash + two-way linked list OrderedDict
class LRUCache(collections.OrderedDict):
    def __init__(self,capacity):
        super().__init__()
        self.capacity = capacity
    def get(self,key):
        if key not in self:
            return -1
        self.move_to_end(key)
        return self[key]
    def put(self,key,value):
        if key in self:
            self.move_to_end(key)
        self[key] = value
        if len(self) > self.capacity:
            self.popitem(last=False)
  1. Self realization
#Bidirectional linked list
class DLinkedNode:
    def __init__(self,key=0,value=None):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None
    
class LRUCache:
    #Initialize LRU cache with positive integer as capacity
    def __init__(self,capacity):
        self.cache = dict()
        #Pseudo header node; Pseudo tail node
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head
        self.capacity = self.capacity
        self.size = 0
        
    #If the keyword key exists in the cache, the value of the keyword is returned; otherwise, - 1 is returned
    def get(self,key):
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self.moveToHead(node)
        return node.value
        
    #If the key already exists, change its value; If the key does not exist, insert the key value into the cache; If the insertion results in capacity overrun, the longest unused keyword will be expelled
    def put(self,key,value):
        if key not in self.cache:
            node = DLinkedNode(key,value)
            self.cache[key] = value
            self.addToHead(node)
            self.size += 1
            if self.size > self.capacity:
                removed = self.removeTail()
                self.cache.pop(removed.key)
                self.size -= 1
        else:
            node = self.cache[key]
            node.value = value
            self.moveToHead(node)
        
    def addToHead(self,node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node
        
    def removeNode(self,node):
    	node.prev.next = node.next
        node.next.prev = node.prev
        
    def moveToHead(self,node):
        self.removeNode(node)
        self.addToHead(node)    	
    
    def removeTail(self):
        node = self.tail.prev
        self.removeNode(node)
        return node

Monotone stack structure

Stack: first in, then out.

Monotone stack: every time a new element is put into the stack, the elements in the stack remain orderly (monotonic increasing or monotonic decreasing)

Monotone stack only deals with one typical problem, Next Greater Element.

Monotone stack template

Give you an array nums and return an equal length result array. The corresponding index in the result array stores the next larger element. If there is no larger element, save - 1.

def nextGreaterElement(nums):
    n = len(nums)
    res = [0 for i in range(n)]
    s = []
    for i in range(n-1,-1,-1):
        while s and s[-1] <= nums[i]:
            s.pop()
        res[i] = -1 if not s else s[-1]
        s.append(nums[i])
    return res

Next larger element I, T496

def nextGreaterElement(nums1,nums2):
    res = {}
    stack = []
    for num in reversed(nums2):
        while stack and num >= stack[-1]:
            stack.pop()
        res[num] = stack[-1] if stack else -1
        stack.append(num)
    return [res[num] for num in nums1]

Daily temperature, T739

Array T stores the temperature in recent days, returns the equal length array, and calculates how many days to wait for a warmer temperature for each day. If not, fill in 0

def dailyTemperatures(T):
    n = len(T)
    res = [0 for i in range(T)]
    s = []
    for i in range(n-1,-1,-1):
        while s and T[s[-1]] <= T[i]:
            s.pop()
        res[i] = 0 if not s else s[-1]-i
        s.append(i)
    return res

[circular array], next larger element II, T503

Common routine: double the length of the array

def nextGreaterElements(nums):
    n = len(nums)
    res = [0 for i in range(n)]
    stack = []
    for i in range(2*n-1,-1,-1):
        while s and s[-1] <= nums[i % n]:
            s.pop()
        res[i % n] = s[-1] if s else -1
        s.append(nums[i%n])
    return res

Monotone queue

Monotone queue mainly solves the problems related to sliding window

Sliding window max, T239

Title: given the array nums and positive integer k, there is a window with size k sliding from left to right on nums. Please output the maximum value of k elements in each window.

#Implement monotonic queue data structure
class MonotonicQueue:
    def __init__(self):
        self.q = LinkedList()
    def push(self,n):
        while q and q.getLast() < n:
            q.pollLast()
        q.addLast(n)
    def max_(self):
        rerurn q.getFirst()
    def pop(self,n):
        if n == q.getFirst:
            q.pollFirst()
    
def maxSlidingWindow(nums,k):
    window = MonotonicQueue()
    res = []
    for i in range(len(nums)):
        #Fill the first k-1 first
        if i < k - 1:
            window.push(nums[i])
        else:
            window.push(nums[i])
            res.append(window.max_())
            window.pop(nums[i-k+1])
    return res

Official answer:

def maxSlidingWindow(nums,k):
    n = len(nums)
    q = collections.deque()
    for i in range(k):
        while q and nums[i] >= nums[q[-1]]:
            q.pop()
        q.append(i)
    res = [nums[q[0]]]
    for i in range(k,n):
        while q and nums[i] >= nums[q[-1]]:
            q.pop()
        q.append(i)
        while q[0] <= i - k:
            q.popleft()
        res.append(nums[q[0]])
    return res

Binary heap implementation priority queue

Binary reactor

[complete binary tree stored in the list] binary heap is logically a complete binary tree, but stored in an array. In the general linked list binary tree, we operate on the pointer of the node, while in the array, we take the array index as the pointer.

Main operation of binary reactor: sink; swim (floating up)

Binary heap is mainly used for heap sorting; Priority queue

Note: the first index 0 is empty

#Array bit 0 is empty
#Parent node index
def parent(root):
    return root // 2
#Left child index
def left(root):
    return root * 2
#Right child index
def right(root):
    return root * 2 + 1

Maximum heap: each node is greater than or equal to its two child nodes

Minimum heap: each node is less than or equal to its two child nodes

Priority queue

Nature: when an element is inserted or deleted, the elements are sorted automatically.

Underlying principle: operation of binary stack

Main API: insert inserts an element, and delMax deletes the largest element (the smallest heap is the smallest element deleted by delMin)

Realize floating swim and sinking sink

For the largest reactor, the following conditions may destroy its nature:

(1) If A node A is smaller than its child node, then A does not deserve to be the parent node and should sink

(2) If A node A is larger than its parent node, A should not be A child node and should float up

Implement delMax and insert

insert: add the element to be inserted to the bottom of the heap and float it up to the correct position

delMax: swap element A at the top of the heap with the last element B at the bottom of the heap, then delete A, and finally let B sink to the correct position

class MaxPQ:
    def __init__(self,N=0,pq=[]):
        self.N = N
        self.pq = pq
    def parent(self,i):
        return i // 2
    def left(self,i):
        return i * 2
    def right(self,i):
        return i * 2 + 1    
    #Swap two elements
    def exch(self,i,j):
        pq[i],pq[j] = pq[j],pq[i]
    #Compare two elements
    def less(self,i,j):
        return pq[i] < pq[j]
    #Float up    
	def swim(self,k):
    	while (k > 1) and MaxPQ.less(MaxPQ.parent(k),k):
            MaxPQ.exch(MaxPQ.parent(k),k)
            k = MaxPQ.parent(k)
    #sink
    def sink(self,k):
        while MaxPQ.left(k) <= N:
            older = MaxPQ.left(k)
            if (MaxPQ.right(k) <= N) and (MaxPQ.less(older,MaxPQ.right(k))):
                older = maxPQ.right(k)
            if maxPQ.less(older,k):
                break
            maxPQ.exch(k,older)
            k = older
    #Insert a new element
    def insert(self,e):
        N += 1
        pq[N] = e
        MaxPQ.swim(N)
    #Delete heap top element
    def delMax(self):
        maxValue = pq[1]
        MaxPQ.exch(1,N)
        pq[N] = None
        N -= 1
        maxPQ.sink(1)
        return maxValue

Queue implementation stack, T225

Queue: first in first out

Stack: first in, last out

class MyStack:
    def __init__(self):
        self.q = []
        self.topelem = 0
        
    def push(self,x):
        self.q.append(x)
        self.topelem = x
        
    def top(self):
        return self.topelem
        
    def pop(self):
        size = len(self.q)
        while size > 2
            self.q.append(self.q.pop(0))
            size -= 1
        self.topelem = self.q[0]
        self.q.append(self.q.pop(0))
        return self.q.pop(0)
        
    def empty(self):
        return not self.q

pop time complexity is O(N), others are O(1)

Stack implementation queue, T232

class MyQueue:
    def __init__(self):
        self.s1 = []
        self.s2 = []
        
    #Push element x to the end of the queue
    #Add to s1 in sequence
    def push(self,x):
    	self.s1.append(x)
    
    #Returns the element at the beginning of the queue
    #When s2 is empty, all elements of s1 can be taken out and added to s2
    def peek(self):
    	if not self.s2:
            while self.s1:
                self.s2.push(self.s1.pop())
        return self.s2[-1]
        
    #Removes and returns an element from the beginning of the queue
    def pop(self):
    	self.peek()
        return self.s2.pop()
        
    #Is the queue empty
    def empty(self):
        return (not self.s1) and (not self.s2)

The worst time complexity is O(N), but the average time complexity is O(1), because an element can only be moved once at most.

Topics: Python data structure leetcode