本章主要涉及到的问题是线性数据结构,主要包括栈(stack),队列(queue),双端队列(deque)和列表(list),它们数据的排列顺序取决于其添加或删除单个数据的方式。线性数据结构可以被认为有两个“端点”,我们可以给这两个端点命名为 “前端点”,“后端点” 或者 “左端点,“右端点” 亦或者 “上端点”,“下端点”。
各种线性数据结构的主要区别在于当我们增加或者删除新的项时,它们是如何改变的。譬如某种数据结构只允许在后端点添加新的项,而另一种数据结构只允许在前端点添加新的项。
堆栈
定义
堆栈(stack)又称为栈或堆叠,是计算机科学中的一种抽象数据类型,只允许在有序的线性数据集合的一端(称为堆栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作。堆栈常用一维数组或链表来实现。
堆栈在日常情况下都会有发生。几乎所有自助餐厅都有一叠盘子,新盘子会被放在顶部,客人在使用盘子的时候需要从顶部开始拿。想象一下桌上的书堆,唯一可见封面的书是最上面的书。要访问堆栈中的其他对象,我们需要删除位于它们顶部的对象。下图显示了另一个堆栈,这个包含许多原始的Python数据对象。
堆栈可以用来逆转一个数组里面元素的排列顺序,因为堆栈的插入顺序和删除顺序相反。将几个数先插入一个堆栈再依次取出,我们就可以得到这些数的逆序排列。由于堆栈具有这种“逆序”的特性,它有很多应用。比如浏览器的“后退”。当我们在浏览网页时,每个网页的URL被压成了堆栈的形式,当我们点击“后退”时,就会返回到之前最先看的网页。
堆栈抽象数据类型
堆栈抽象数据类型由以下结构和操作定义。
- Stack()创建一个新的堆栈,该堆栈为空。它不需要参数,并返回一个空堆栈。
- push(item)将新项目添加到堆栈的顶部。不返回任何内容。
- pop()从堆栈中删除顶层项目。它不需要参数并返回该项目。堆栈已被修改。
- peek()返回堆栈中的顶层项目,但不会将其删除。它不需要参数。堆栈未被修改。
- isEmpty()测试以查看堆栈是否为空。它不需要任何参数,并返回一个布尔值。
- size()返回堆栈中的项目数。它不需要参数,并返回整数。
Python实现
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[-1]
def size(self):
return len(self.items)
上面的python实现我们假设堆栈的顶部在数组的结尾,但是如果顶部在数组的开头的话,push、pop和peek方法将不再有效。这时我们需要对其进行修改。
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)
应用:检查括号是否平衡
括号平衡意味着每个开始半括号都有一个对应的结束半括号,并且括号对儿正确地嵌套了。
平衡的括号对儿:(((())))
不平衡的括号对儿:(()()(()
从左到右处理符号时,最近的左括号必须与下一个右括号匹配。同样,处理的第一个开头符号可能必须等到最后一个与其匹配的符号。结束符与开始符的出现顺序相反,他们从内而外地匹配,因此我们可以用堆栈数据结构来解决这个问题。
思路:从一个空堆栈开始,从左到右处理括号字符串。如果符号是左括号,则将其压入堆栈。另一方面,如果符号是右括号,则弹出堆栈。如果在任何时候堆栈上都没有与结束符号匹配的开始符号,则说明字符串没能够正确的平衡。在末尾时,所有符号都已处理完后,堆栈应为空。
from pythonds.basic import Stack
def parChecher(symbolString):
s = Stack()
balanced = True
index = 0
while index < len(symbolString) and balanced:
symbol = symbolString[i]
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
应用:检查括号是否平衡(通用)
平衡不同种类的括号是更为普遍的问题。例如,在Python中,方括号[ ]用于列表;花括号{ }用于字典;圆括号( )用于元组和算术表达式。只要每个符号保持同类型的打开和关闭关系,就可以混合使用它们。
平衡的括号对儿:{ { ( [ ] [ ] ) } ( ) }
不平衡的括号对儿:( [ ) ]
我们只需要在上面的代码修改一下即可实现一般情况下的检测括号是否平衡。
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 match(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)
应用:任意进制转换
转化为二进制:
from pythonds.basic import Stack
def divideBy2(decNumber):
remstack = Stack()
while decNumber > 0:
rem = decNumber % 2
remstack.push(rem)
decNumber = decNumber // 2
binstring = ""
while not remstack.isEmpty():
binString = binString + str(remstack.pop())
return binString
任意进制转换:
from pythonds.basic import Stack
def baseConverter(decNumber,base):
digits = "0123456789ABCDEF"
remstack = Stack()
while decNumber > 0:
rem = decNumber % base
remstack.push(rem)
decNumber = decNumber // base
newString = ""
while not remstack.isEmpty():
newString = newString + digits[remstack.pop()]
return newString
应用:中缀表达式(infix)转换为后缀表达式(postfix)或者前缀表达式(prefix)
队列
队列是项目的有序集合,其中新项目的添加发生在一端(称为“后部”),而现有项目的去除发生在另一端(通常称为“前部”)。当元素进入队列时,从后部开始,朝着前部前进,一直等到那是要删除的下一个元素时(类似于排队)。
队列中最新添加的项目必须在集合的末尾等待。等待时间最长的物品在最前面。这种排序原理被称为FIFO,先进先出,也称为“先到先得”。队列必须从头开始依次进行,不能够插入或者跳过某个元素。
队列抽象数据类型
队列抽象数据类型由以下结构和操作定义。
- Queue()创建一个空的新队列。它不需要任何参数,并返回一个空队列。
- enqueue(item)将新项目添加到队列的后面。它需要该项目,但不返回任何内容。
- dequeue()从队列中删除最前面的项目。它不需要参数并返回该项目。队列已修改。
- isEmpty()测试以查看队列是否为空。它不需要任何参数,并返回一个布尔值。
- size()返回队列中的项目数。它不需要参数,并返回整数。
Python实现
class Queue:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def enqueue(self, item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
应用:热土豆问题
热土豆问题:有一个热土豆在几名同学手中传递,当传过num次数后,热土豆在谁手里就退出游戏,循环传递 ,直到剩下最后一个就是赢家。
from pythonds.basic import Queue
def hotPotato(namelist, num):
simqueue = Queue()
for name in namelist:
simqueue.enqueue(name)
while simqueue.size() > 1:
for i in range(num):
# 先dequeue再enqueue保证了拿到过热土豆的
# 小朋友重新加入队列(位于队列的末尾)
simqueue.enqueue(simqueue.dequeue())
simqueue.dequeue()
return simqueue.dequeue()
应用:打印机任务分配
双端队列
双端队列是类似于队列的项的有序集合,它有一个前端(front)和一个后端(rare)。使双端队列不同于队列的是添加和删除项目的无限制性质。我们可以在前端或后端添加新的项。同样,可以从任一端删除现有项目。从某种意义上说,这种混合线性结构在单个数据结构中提供了堆栈和队列的所有功能。下图显示了Python数据对象的双端队列。
重要的是要注意,即使双端队列可以假定堆栈和队列的许多特征,但它并不需要强制执行的LIFO和FIFO等排序规则。双端队列的使用取决于如何一致地使用添加和删除操作。
双端队列抽象数据类型
双端队列抽象数据类型由以下结构和操作定义。
- Deque()创建一个空的新双端队列。它不需要任何参数,并返回一个空的双端队列。
- addFront(item)在双端队列的前面添加一个新项。它需要该项目,但不返回任何内容。
- addRear(item)在双端队列的后面添加一个新项。它需要该项目,但不返回任何内容。
- removeFront()从双端队列中删除前一项。它不需要参数并返回该项目。双端队列已修改。
- removeRear()从双端队列移除后部项目。它不需要参数并返回该项目。双端队列已修改。
- isEmpty()测试以查看双端队列是否为空。它不需要任何参数,并返回一个布尔值。
- size()返回双端队列中的项目数。它不需要参数,并返回整数。
Python实现
class Deque:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def addFront(self, item):
self.items.append(item)
def addRear(self, item):
self.items.insert(0,item)
def removeFront(self):
return self.items.pop()
def removeRear(self):
return self.items.pop(0)
def size(self):
return len(self.items)
应用:检查回文字符串
如果给定的字符串是回文,返回true,反之,返回false。
如果一个字符串忽略标点符号、大小写和空格,正着读和反着读一模一样,那么这个字符串就是palindrome(回文),例如radar,toot,and madam。
解决此问题的方法之一是使用双端队列存储字符串的字符。我们将从左至右处理字符串,并将每个字符添加到双端队列的后面。此时,双端队列的行为将非常类似于普通队列。但是,我们现在可以使用双端队列的双重功能,即双端队列的前端保存着字符串的第一个字符,后端保存着字符串的最后一个字符。因此我们可以通过移除并比较这两个字符来实现检查的目的。
def palchecker(string):
chardeque = Deque()
for ch in string:
chardeque.addRear(ch)
stillEqual = True
while chardeque.size()>1 and stillEqual:
first = chardeque.removeFront()
last = chardeque.removeRear()
if first != last:
stillEqual = False
return stillEqual
列表
在讨论基本数据结构的整个过程中,我们使用Python列表来实现所呈现的抽象数据类型。列表是一种功能强大但简单的收集机制,可提供各种操作。但是,并非所有的编程语言都包括列表集合。在这些情况下,列表的概念必须通过编程来实现。
列表是项的集合,其中每个项相对于其他项都具有相对位置。更为具体地来说,我们将这种类型的列表称为无序列表。我们可以将列表视为具有第一项,第二项,第三项,依此类推。我们还可以表示出列表的开头(第一项)或列表的结尾(最后一项)。为简单起见,我们假定列表不能包含重复项。
例如,整数54、26、93、17、77和31的集合可能表示考试分数的简单无序列表。请注意,我们已将它们编写为以逗号分隔的值,这是显示列表结构的常用方法。
无序列表抽象数据类型
无序列表抽象数据类型由以下结构和操作定义。
- List()创建一个空的新列表。它不需要参数,并返回一个空列表。
- add(item)将一个新项目添加到列表中。它需要该项目,什么也不返回。假设该商品尚未在列表中。
- remove(item)从列表中删除项目。它需要该项目并修改列表。假设该项目存在于列表中。
- search(item)在列表中搜索项目。它需要该项并返回一个布尔值。
- isEmpty()测试以查看列表是否为空。它不需要任何参数,并返回一个布尔值。
- size()返回列表中的项目数。它不需要参数,并返回整数。
- append(item)将一个新项目添加到列表的末尾,使其成为集合中的最后一个项目。它需要该项目,什么也不返回。假设该商品尚未在列表中。
- index(item)返回项目在列表中的位置。它需要该项并返回索引。假设该项目在列表中。
- insert(pos,item)在位置pos处将一个新项目添加到列表中。它需要该项目,但不返回任何内容。假设该商品尚未在列表中,并且已有足够的现有商品具有位置pos。
- pop()删除并返回列表中的最后一项。它什么也不需要,并返回一个项目。假设列表中至少有一项。
- pop(pos)删除并返回位置pos处的项目。它需要位置并返回项目。假设该项目在列表中。
无序列表的实现
我们将使用链表来构建无序列表。回想一下,我们需要确保我们可以保持项的相对位置。但是,这不意味着我们在内存中需要连续的存储空间。例如,考虑图1所示的项目集合。看来这些值是随机放置的。如果我们可以在每个项目中维护一些明确的信息,即下一个项目的位置(请参见图2),那么可以通过简单地跟踪从一个项目到下一个项目的链接来表示每个项目的相对位置。
重要的是要注意,列表的第一项的位置必须明确指定。一旦我们知道第一个项目在哪里,第一个项目就可以告诉我们第二个项目在哪里,同样,最后一项需要知道没有下一项。
The Node Class
链表实现的基本构件是节点(node)。每个节点对象必须至少包含两条信息。首先,节点必须包含列表项本身的信息。我们将其称为节点的数据字段。此外,每个节点必须保留下一个节点的位置信息。要构建节点,需要提供该节点的初始数据值。
class Node:
def __init__(self, initdata):
self.data = initdata
self.next = None
def getData(self):
return self.data
def getNext(self):
return self.next
def setData(self, newdata):
self.data = newdata
def setNext(self, newnext):
self.next = newnext
当node只能够next指向None时,这表明没有下一个节点。
Python实现
- 无序列表类
head=None意味着在开始的时候无序列表中没有存储任何项。图5表示的是一个空的无序列表,此时head指向None。图六表示了一个无序列表,此时head指向列表中的第一个项。需要注意的是,列表类本身并不包含任何节点对象。相反,它仅包含对链接结构中第一个节点的单个引用。
def UnorderedList:
def __init__(self):
self.head = None
- isEmpty方法
检查无序列表是否为空,我们只需要检查head是否指向None即可。None意味着整个数组的结尾。
def isEmpty(self):
return self.head == None
- add方法
因为本身是无序列表,所以新元素的添加并没有位置要求。将新元素添加到列表的最开始会更容易实现。实现时,我们首先要添加一个Node类用来存储本身的数据信息和下一节点的位置信息。然后将新添加节点指向之前head节点所链接的Node,再让head节点指向新添加的Node,这样我们就完成了添加新节点的过程。
def add(self, item):
temp = Node(item)
temp.setNext(self.head)
self.head = temp
- size,search,remove方法
这三个方法的实现基于链表遍历。遍历是指系统访问每个节点的过程。为此,我们需要设定一个指针。当我们访问每个节点时,我们通过“遍历”节点时指针也会相应的移动。
def size(self):
current = self.head()
count = 0
while current != None:
count = count + 1
current = current.getNext()
return count
def search(self, item):
current = self.head
found = False
while current != None and not found:
if current.getData == item:
found = True
else:
current = current.getNext()
return found
remove方法需要两个逻辑步骤。首先,我们需要遍历列表以查找要删除的项目。找到项目后(我们假设它存在),我们必须将其删除。这一步与search方法非常相似。从head开始,通过设置指针,我们遍历列表,直到找到所需的项。这里我们用布尔值found来表明我们是否找到了我们所要找的项。
当found变为True时,current指向我们需要删除的节点。但是,我们如何将其删除?一种可能性是我们修改该节点的数据信息。这种方法的问题是节点数将不再与项数匹配,所以通过删除整个节点来删除项会更好。
为了删除包含该项目的节点,我们需要修改前一个节点中的链接,以使其指向当前节点之后的节点。不幸的是,我们没有办法在链表中倒退。由于current指向删除的节点,但我们需要的是前一个节点的信息。解决此难题的方法是在遍历链表时使用两个指针。
def remove(self, item):
current = self.head
previous = None
found = False
while not found:
if current.getData == item:
found = True
else:
previous = current
current = current.getNext()
# 特殊情况:如果需要删除的节点位于第一个位置
# 此时,我们需要修改head节点的指向
if previous == None:
self.head = current.getNext()
else:
previous.setNext(current.getNext())
有序列表抽象数据类型
现在,我们将考虑一种称为有序列表的列表类型。例如,如果整数列表是有序列表(升序),则可以将其写为17、26、31、54、77和93。由于17是最小的项,因此它占据第一个位置。同样,由于93是最大的,因此它位于最后一个位置。
- OrderedList()创建一个新的空列表。它不需要参数,并返回一个空列表。
- add(item)将一个新项目添加到列表中,以确保顺序是正确的。它需要输入,但不返回任何内容。假设该项尚未在列表中。
- remove(item)从列表中删除项目。它需要该项并修改列表。假设该项存在于列表中。
- search(item)在列表中搜索项目。它需要该项并返回一个布尔值。
- isEmpty()测试以查看列表是否为空。它不需要任何参数,并返回一个布尔值。
- size()返回列表中的项目数。它不需要参数,并返回整数。
- index(item)返回项在列表中的位置。它需要该项并返回索引。假设该项目在列表中。
- pop()删除并返回列表中的最后一项。它不需要参数,并返回一个项目。假设列表中至少有一项。
- pop(pos)删除并返回位置pos处的项目。它需要位置并返回项目。假设该项目在列表中。
Python实现
- 有序列表类
class OrderedList:
def __init__(self):
self.head = None
-
search方法
在考虑有序列表的操作时,需要注意,可以使用与无序列表相同的方法来实现isEmpty和size方法,因为它们仅处理列表中的节点数,而与实际项值无关。同样,remove方法也可以正常工作,因为我们仍然需要找到该项目,然后在节点周围链接以将其删除。剩下的两个方法,search和add方法,将需要进行一些修改。要搜索无序链表,需要遍历所有的节点,直到找到所需的项或遍历完所有的节点为止。事实证明,相同的方法实际上适用于有序列表。但是,如果该项不在列表中,我们可以利用排序的优势来尽快停止搜索。比如我们想要找45,当遍历到31(小于45)和54(大于45)这两个节点时,我们就可以停止遍历了,因为这是个有序列表,所以列表中不会有45这个项了。
def search(self, item):
current = self.head
found = False
stop = False
while current != None and not found and not stop:
if current.getData == item:
found = True
else:
if current.getData > item:
stop = True
else:
current = current.getNext()
return found
- add方法
假设我们有一个由17、26、54、77和93组成的有序列表,我们想添加31。add方法必须确定新项在26和54之间。如前所述,我们需要遍历链表以查找将要添加新节点的位置。我们知道,当节点用完(当前变为None)或当前节点的值大于要添加的项时,就会发现该位置。在我们的示例中,看到值54会使我们停止。
def add(self, item):
current = self.head
previous = None
stop = False
while current != None and not stop:
if current.getData() > item:
stop = True
else:
previous = current
current = current.getNext()
temp = Node(item)
if previous == None:
temp.setNext(self.head)
self.head = temp
else:
temp.setNext(current)
previous.setNext(temp)