基础数据结构(1) - 线性数据结构和栈

159 阅读5分钟

线性数据结构

像栈、队列、双端队列和列表都是有序的数据集合,他们的元素顺序取决于添加顺序或移除顺序。当有元素被添加进来时,它与前后元素的相对位置将保持不变,这样的数据集合被称为线性数据结构

线性数据结构可以看作有两端,这两端可以被称作“左端”和“右端”,也可以称作“前端”“后端”“顶端”“底端”。名字并不重要,真正区分线性数据结构的是元素的添加方式和移除方式,尤其是他们的位置,因为某种数据结构可能只允许在一端添加或删除新元素。

什么是栈

也被称为“下推栈”,它是一个有序集合,添加或删除操作总是发生在同一端。我们可以将想象成一摞盘子,我们会将最顶端的盘子最先取出,而最底端的盘子最后取出。

中的元素离底端越近,代表其在中的时间就越长,这种后进先出的排序原则被称为LIFO(last-in first-out)。即最近添加的元素靠近顶端,旧元素靠近底端

下面是由原生的Python构成的

image.png 下面展示了Python数据对象栈的创建过程和拆除过程,他的元素的插入顺序和移除顺序相反。我们将这一特性叫做栈的反转特性

image.png

栈的抽象数据类型

支持以下操作:

Stack() 创建一个空栈。它不需要参数,且会返回一个空栈。

push(item) 将一个元素添加到栈的顶端。它需要一个参数 item,且无返回值。

pop() 将栈顶端的元素移除。它不需要参数,但会返回顶端的元素,并且修改栈的内容。

peek() 返回栈顶端的元素,但是并不移除该元素。它不需要参数,也不会修改栈的内容。

isEmpty() 检查栈是否为空。它不需要参数,且会返回一个布尔值。

size() 返回栈中元素的数目。它不需要参数,且会返回一个整数。

用Python来实现栈

Python列表是一个有序集合,所以我们在用他实现栈的时候只需要考虑定义他的哪一边为顶端就可以了。

假设列表的尾部是顶端

所有的操作可以利用append和pop实现。

class Stack:
    def _init(self):
        self.items = []
    def isEmpty(self):
        return self.items = []
    def push(self, item):
        self.items.append(item)
    def pop(self):
        return self.items.pop()
    def peek(self):
        return self.items[len(self.items) - 1]
    def size(self):
        return len(self.items)

>>> s = Stack()
>>> s.isEmpty()
True
>>> s.push(4) // [4]
>>> s.push('dog') // [4, 'dog']
>>> s.peek()
'dog'
>>> s.push(True) // [4, 'dog', True]
>>> s.size
3
>>> s.isEmpty()
False
>>> s.push(8.4) // [4, 'dog', True, 8.4]
>>> s.pop() // [4, 'dog', True]
8.4
>>> s.pop() // [4, 'dog']
True
>>> s.size()
2

假设列表的头部是顶端

不能直接使用append和pop方法,必须通过pop和insert方法显式地访问下标为0的元素,也就是列表中的第一个元素。

class Stack:
    def _init_(self):
        self.items = []
    def isEmpty(self):
        return self.items = []
    def push(self,item):
        self.items.insert(0, item)
    def pop(self):
        return self.items.pop(0)
    def peek(self):
        return self.items[0]
    def size(self):
        return len(self.items)

总结:虽然上面两种方法逻辑上相同,但在性能方面还是具有差异性的。append和pop方法的时间复杂度都是O(1),这意味着不论有多少个元素,这两个操作都会在恒定时间内完成。而insert(0)和pop(0)的时间复杂度是O(n),元素越多就会越慢。

匹配括号

接下来我们要编写一个算法,它从左到右读取一个括号串,然后判断其中的括号是否匹配

需要注意的是,当从左到右处理括号时,最右边的左括号会与接下来遇到的第一个右括号相匹配,而第一个左括号则要等到处理至最后一个位置的右括号才能完成匹配。也就是说,相匹配的右括号与左括号出现的顺序相反。所以我们可以使用来解决这个括号匹配的问题。

image.png 我们首先由一个空栈开始,如果遇到左括号就通过push加入中,用来占位并表示后面需要有一个与之匹配的右括号。反之,如果遇到右括号就调用pop操作。只要栈中的所有左括号都能与右括号进行匹配,那么整个括号串就是匹配的;如果中有任何一个左括号占不到匹配的右括号,那么括号串就是不匹配的。当处理完匹配的括号串以后,应该是空的。

from pythonds.basic import Stack

def parChecker(symbolString):
    s = Stack()
    balanced = True
    index = 0
    while index < len(symbolString) and balanced:
        symbol = symbolString[index]
        if(symbol) == "(":
            s.push(symbol)
        else:
            if s.isEmpty():
                balanced = False
            else:
                s.pop()
        index = index + 1
    if balanced and s.isEmpty():
        return True
    else:
        return False

parChecker最终会通过返回一个True或False来判断括号串是否匹配。如果当前是左括号就会被压入中,而从中移除仅用了一个pop,由于移除的元素一定是之前遇到的左括号,所以并没有用到pop的返回值。最后只要所有的括号都匹配并且为空,就返回True,否则返回False。

匹配符号

匹配符号和匹配括号的处理差不多,唯一的区别就是,当出现右符号时,必须检测是否与左符号相匹配,如果不匹配则整个符号串也就不匹配。

from pythonds.basic import Stack
def parChecker(symbolString):
    s = Stack()
    balanced = True
    index = 0
    
    while index < len(symbolString) and balanced:
        symbol = symbolString[index]
        if symbol in "([{":
            s.push(symbol)
        else:
            if s.isEmpty():
                balanced = False
            else:
                top = s.pop()
                if not matches(top, symbol):
                    balanced = False
                    index = index + 1
                if balanced and s.isEmpty():
                    return True
                else:
                    return False
def matches(open, close):
    opens = "([{"
    closers = "}])"
    return opens.index(open) == closers.index(close)

前序、中序和后序表达式

对于 B * C 这样的算数表达式,由于乘号出现在两个变量之间,所以这种表达式被称作中序表达式

对于 A + B * C 这样的表达式,在运算时,会出现运算符的优先级问题。即先算乘除后算加减。

但计算机却需要明确地知道以何种顺序来进行何种运算,所以这个表达式正确的算法应该是(A + (B * C)),这种杜绝歧义的写法被称为完全括号表达式

以 A + B 为例,如果我们将符号提前,写作 +AB ,那么我们称为前序表达式;如果将符号放在后面,即 AB+ ,那么我们称为后序表达式

在运算中,只有中序表达式需要额外的符号来消除歧义,而前序表达式后序表达式的运算顺序是完全由运算符的位置决定的。所以说,中序表达式是最不理想的算式表达式。

image.png