栈和队列

  一种重要的线性结构;后进先出。特殊的线性表,只能在表尾进行插入(push)和删除(pop)操作。 表尾称为栈顶(top),表头称为栈底(bottom)。一般用顺序表实现。

清空一个栈:s->base = s->top

销毁一个栈:释放其所占的物理内存空间。

for(i=0, i<s->stackSize) {free(s->base); s->base++}

s->base = s->top = NULL; s->stackSize = 0

 

用栈实现二进制转十进制

def Bin2Dec(BinStrs):
    strStack = []
    for elem in BinStrs:
        strStack.append(elem)
    DecRes = 0
    n = 0
    while strStack:
        DecRes += eval(strStack.pop())*(2**n)
        n += 1
    return DecRes

BinInputs = input('Binary input:')
print('Decade output:', Bin2Dec(BinInputs))

 

栈的链式存储结构,将栈顶放在单链表的头部,即栈顶指针和单链表的头指针合二为一。

 

逆波兰表达式RPN:利用栈来进行运算的数学表达式。(中缀表达式转换为后缀表达式)

class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        Stack = []
        operations = {'+','-','*','/'}
        # 遇到操作符就pop出两个元素进行操作,结果再push进栈
        for token in tokens:
            if token in operations:
                right = Stack.pop()
                left = Stack.pop()
                tmp = 0
                if token == '+':
                    tmp = left + right
                elif token == '-':
                    tmp = left - right
                elif token == '*':
                    tmp = left*right
                else:  # 整除要判断正负性
                    if right*left > 0:
                        tmp = left//right
                    elif right*left < 0:
                        tmp = -(-left//right)
                Stack.append(tmp)
            else:
                Stack.append(int(token))
        return Stack.pop()

 

中缀表达式转换为后缀表达式

1.遇到操作数,直接输出; 
2.栈为空时,遇到运算符,入栈; 
3.遇到左括号,将其入栈; 
4.遇到右括号,执行出栈操作,并将出栈的元素输出,直到弹出栈的是左括号,左括号不输出; 
5.遇到其他运算符’+”-”*”/’时,弹出所有优先级大于或等于该运算符的栈顶元素,然后将该运算符入栈; 
6.最终将栈中的元素依次出栈,输出。 

def middle2behind(expresssion):  
    result = []             # 结果列表
    stack = []              # 栈
    operations = {'+','-','*','/','(',')'}
    for item in expression: 
        if item not in operations:  # 数字直接输出
            result.append(item) 
        else:                     # 如果是操作符
            if len(stack) == 0:   # 若栈空,直接入栈
                stack.append(item)
            elif item in '*/(':   # 如果当前字符为*/(,直接入栈
                stack.append(item)
            elif item == ')':     # 如果右括号则全部弹出(碰到左括号停止,左括号不输出)
                t = stack.pop()
                while t != '(':   
                    result.append(t)  
                    t = stack.pop()
            # 如果当前字符为加减且栈顶为乘除,则开始弹出
            elif item in '+-' and stack[-1] in '*/':
                if stack.count('(') == 0:  # 如果没有有左括号,全部弹出     
                    while stack:
                        result.append(stack.pop())
                else:                      # 如果有左括号,弹出到左括号为止
                    t = stack.pop()  
                    while t != '(':
                        result.append(t)
                        t = stack.pop()
                    stack.append('(')  # 左括号pop出来了,再补回去
                stack.append(item)  # 弹出操作完成后将‘+-’入栈
            else:
                stack.append(item)# 其余情况直接入栈(如当前字符为+,栈顶为+-)

    # 栈中还有操作符不满足弹出条件,把栈中的东西全部弹出
    while stack:
        result.append(stack.pop())
    # 返回字符串
    return "".join(result)

  

单调栈:保持先进后出的栈特性,每次新元素入栈后,栈内的元素都保持有序。

处理Next Greater Element问题。给定一个数组a,返回一个等长数组b,b中存储a中相同索引的元素的下一个更大元素,如果没有就存-1。例如a=[2,1,2,4,3],返回b=[4,2,4-1,-1]。

暴力解,两层遍历O(n2),不写了。

单调栈,每个元素入栈一次,最多pop一次,O(n)。

def nextGreaterElement(nums):
    if not nums:
        return []
    
    n = len(nums)
    ans = [-1] * n
    stack = []  # stack
    
    for i in range(n-1, -1, -1):  # nums中的元素倒着入栈,就正着出栈
        while stack and stack[-1] <= nums[i]:  # 只要栈顶元素不大于当前元素,就pop出去
            stack.pop()   # 所以stack始终被维护成一个从底到顶单调递减的栈
            
        ans[i] = stack[-1] if stack else -1   # 栈顶元素是第一个比当前元素大的元素。按ans索引从后往前赋值
        stack.append(nums[i])  # 当前元素入栈,等着和nums中i之前位置的元素比较
    return ans

  

问题改为,返回一个数组,存的是每个元素距其Next Greater Element的距离。例如a=[2,1,2,4,3],返回b=[3,1,1,0,0]。

def distance_nextGreaterElement(nums):
    if not nums:
        return []
    
    n = len(nums)
    ans = [-1] * n
    stack = []  # stack
    
    for i in range(n-1, -1, -1):  # nums中的元素倒着入栈,就正着出栈
        while stack and nums[stack[-1]] <= nums[i]:  # 只要栈顶索引对应的元素不大于当前元素,就把索引pop出去
            stack.pop()   # 所以stack始终被维护成一个索引对应值单调递减的栈
            
        ans[i] = stack[-1] - i if stack else 0   # 栈顶元素是第一个比当前元素大的,按索引从后往前赋距离
        stack.append(i)  # 当前元素索引入栈
    return ans

  

还是Next Greater Element问题,假设给定的数组是环形的。那么某个元素的Next Greater Element就有可能在它本身之前了。例如a=[2,1,2,4,3],返回b=[4,1,1,-1,4]。

可以直接在原数组后面再挂一个原数组,还用上面的思路求解。例如 [2,1,2,4,3, 2,1,2,4,3],结果为[4,2,4,-1,4, 4,2,4,-1,-1]取前一半即可。

或者直接用索引mod n的技巧模拟这种double数组的情况。

ef nextGreaterElement(nums):
    if not nums:
        return []
    
    n = len(nums)
    ans = [-1] * n
    stack = []  # stack
    
    # 假设数组长度翻倍了,还是从后往前
    # 遍历后半部分求解的时候没有按循环数组取下一个大值,但在遍历到前半部分的时候就把正确结果覆盖了
    # 相当于ans更新了两遍
    for i in range(2*n-1, -1, -1):  
        while stack and stack[-1] <= nums[i % n]:  
            stack.pop()   
            
        ans[i % n] = stack[-1] if stack else -1 
        stack.append(nums[i % n])  # 索引 mod n 取到在nums中真实位置的值 
    return ans

  

 

 

队列

  先进先出,可以用线性表或链表实现。一般用链表实现。队列头指针front指向头节点,队列尾指针rear指向尾节点。链表尾插头出。

队列的顺序存储结构:循环队列解决假溢出。

还是用数组,其实只需要让 front 和 rear 不断加 1(插入改rear、弹出改front),如果超出了地址范围就从头开始(避免假溢出),直到 front 和 rear 相遇就满了。可以采取 mod 运算实现。

- (rear+1) % queueSize

- (front+1) % queueSize

 

双端队列:这种混合的线性数据结构拥有栈和队列各自拥有的所有功能。插入和删除操作的规律性需要由用户自己维持。

应用:判断回文字符

 

单调队列: 队列中元素单调递增或者递减。单调队列的 push 方法依然在队尾添加元素,但是要把前面比新元素小的元素都删掉。如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序。

回顾一下leetcode 239 滑动窗口最大值。窗口滑动的过程中新增一个数又减少一个数,这时候最值的更新就不是那么快可以直接算,要重新遍历窗口中的所有数据。

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        if not nums:
            return []
        
        n = len(nums)
        res = []
        window = []  # 双端队列实现单调队列
        
        for i in range(n):
            if i >= k and window[0] <= i-k:
                window.pop(0)  # 从第二个窗口开始需要pop最左边滑出窗口的值

            while window and nums[window[-1]] <= nums[i]:  # 维护递减的单调队列
                window.pop()  # popright
            window.append(i)   # 新的值进来,此时window最左端就是当前窗口的最大值
            
            if i >= k-1:   # 从第一个窗口开始记录结果
                res.append(nums[window[0]])
    
        return res

  

 

优先队列:正常入队,按优先级出队。当插入或者删除元素的时候,元素会自动排序。实现机制为堆或者二叉搜索树。

大/小顶堆的性质就是每个父节点都大于等于/小于等于其子节点。维护堆的操作就是下沉和上浮。

特别的一点是,元素存储在数组里,数组的索引作为指针,注意第一个索引0空着不用。给定一个节点root,其孩子节点为2*root和2*root+1;同理给定一个root,其父节点为root//2。

大顶堆的常用操作为 insert 和 delMax;小顶堆的常用操作为 insert 和 delMin。

以大顶堆为例,如果一个节点的val比父节点的val大,需要上浮;如果小于其孩子节点,需要下沉。

伪代码

def swim(k):
    while k > 1 and less(parent(k), k):  # 如果浮到顶了就不用动了,如果k比其父节点大,把k换上去
        exchange(parent(k), k)
        k = parent(k)
def sink(k):
    while left(k) <= N:  # 沉到堆底就不用沉了
        larger = left(k)     # 假设左边节点较大
        if right(k) <= N and less(larger, right(k)):
            larger = right(k)   # 如果右边节点存在且更大,就更新larger
        if less(larger, k):   # 和k比较,如果k比其孩子节点都大,不用下沉了
            break
        exchange(k, larger)   # 否则的话往larger方向下沉k节点
        k = larger

  

插入和删除

def insert(e):
    N ++  # 先把元素放最后
    pq[N] = e
    swim(N)  # 上浮到正确位置


def delMax()
    Max = pq[1]   # 堆顶最大
    exchange(1, N)  # 换到最后,删除
    pq[N] = null
    N -- 

    sink(1)   # 让pq[1]沉到正确位置
    return Max

  

 

堆的各种实现形式及对应的操作效率

 

 

 常用数据结构及其各种操作的时间复杂度

 

posted @ 2019-07-22 23:49  王朝君BITer  阅读(324)  评论(0编辑  收藏  举报