数据结构是我们实现算法的基础,在学习算法前我们先简单回顾一下数据结构的基础知识,具体内容网上都有博文详述,在此不过多赘述。
数据结构分为逻辑结构和存储(物理)结构:
逻辑结构:
-集合(set)
-线性结构(linearity)
-树形结构(tree)
-图形结构 (graph)
存储结构:
-顺序(array)
-链式(linked list)
-索引(index)
-散列(hash)
在最初学习数据结构时,相信大家一定都是从线性表开始的,因为它是最简单、最基本、最常用的一种线性结构,根据存储方法可分为顺序表和链表,基本操作包括插入、删除和检索等。
为什么有这样的划分呢?我认为根本上是因为计算机存储的物理特性。
顺序存储结构把逻辑上相邻的结点存储在物理位置相邻的存储单元里,借助程序设计语言中的数组来实现。
链式存储结构则不要求逻辑相邻的结点物理位置也相邻,而是由附加的指针字段表示(存储相邻结点的地址),借助指针(c\c++)或引用(Java\Python)来实现。
以上两种存储结构,在不同情境下各有优劣:
– 从内存开销(空间复杂度)上来看,链表需要多附加指针字段(存放地址),因此对于64位寻址能力的系统来说,每个结点每增加一个指针(单链表)或是一个引用,都需要额外8个字节的内存。
– 从运算速度(时间复杂度)上来看,链表做插入和删除操作时效率更高,直接更改结点中指针指向的地址即可(时间复杂度O(1)),而顺序表则有可能需要移动整个数组(时间复杂度O(n))。但是在检索时,顺序表可以根据索引号n直接访问到对应元素(时间复杂度O(1)),而链表则需要从头结点开始依次访问到目标元素(时间复杂度O(n))。当n非常大时,这对程序运行效率的影响是十分巨大的。
– 另外,对于像c\c++\java这样的程序设计语言,由于数组的创建时需要给定大小,因此顺序表是相对固定的;而链表则可以在有元素插入时生成一个结点并与链表连接,是一个相对动态的过程。但其实我们也可以动态的改变顺序表的大小,根据表中元素的个数扩增或缩减数组的大小,这就是所谓的调整数组(resizing array)。Python中的数据结构列表list,就是采用这种思想来设计的动态顺序表,有兴趣的可以参考这篇博客:Python中list的实现,英文原文:Python list implementation。
由于计算机硬件基础的发展,我们对于链表几个几十个字节的额外内存开销,在可以提升运算效率的前提下,一般是可以接受的。但是对于数据表的基本操作,我们如何对其运算效率做到综合最优,这就是算法所需要研究的问题了。
以下是Python 3的实现:
# 顺序表
class List:
def __init__(self):
self.list = []
def __str__(self):
return str(self.list)
def put(self, item):
self.list.append(item)
def size(self):
return len(self.list)
def isEmpty(self):
return self.list == []
# 单链表
class LinkedList:
# 定义结点内部类
class __Node:
item = None
next = None
def __init__(self, item, n):
self.item = item
self.next = n
def __init__(self):
self.__head = self.__Node(None,None)
self.__size = 0
def __str__(self):
p=self.__head.next
l=[]
while p:
l.append(p.item)
p = p.next
return str(l)
# 头插法
def put(self, item):
node = self.__Node(item, self.__head.next)
self.__head.next = node
self.__size += 1
# 删除第一个结点
def remove(self):
node = self.__head.next
self.__head.next = node.next
self.__size -= 1
del node
def size(self):
return self.__size
def isEmpty(self):
return self.__size == 0
# 测试
list=List()
for i in range(0,10):
list.put(i)
print(list.size())
print(list)
# 输出
>>10
>>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 测试
ll=LinkedList()
for i in range(0,10):
ll.put(i)
print(ll.size(),ll)
ll.remove()
print(ll.size(),ll)
# 输出
>>10 [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>9 [8, 7, 6, 5, 4, 3, 2, 1, 0]
这里对单链表的put()方法使用了头插法,如果想要插在最后则需要将所有结点都访问一般,当链表结点个数非常多时,这无疑非常耗时,因此我们可以增加rear字段来指向最后一个结点。但是在删除尾部结点时,我们需要将rear移动到其前一个结点,为了能直接访问到前一个结点,我们可以增加last字段指向前一个结点。
这就是所谓的双向链表,是我们牺牲一点点内存来加速访问速度的产物。
接下来如果我们将head与最后一个结点连接,就构成了双向循环链表,这时候其实可以不再需要rear字段,因为head.last指向的就是尾结点。
# 双向循环链表
class DoubleLoopLinked:
class __Node:
item = None
next = None
last = None
def __init__(self, item, n, l):
self.item = item
self.next = n
self.last = l
def __init__(self):
self.__head = self.__Node(None, None, None)
self.__size = 0
def __str__(self):
p = self.__head.next
l = []
while p != self.__head:
l.append(p.item)
p = p.next
return str(l)
# 尾插法
def insertToTail(self, item):
if self.isEmpty():
node = self.__Node(item, self.__head, self.__head)
self.__head.next = node
self.__head.last = node
else:
node = self.__Node(item, self.__head, self.__head.last)
self.__head.last.next = node
self.__head.last = node
self.__size += 1
# 头插法
def insertToHead(self, item):
if self.isEmpty():
node = self.__Node(item, self.__head, self.__head)
self.__head.next = node
self.__head.last = node
else:
node = self.__Node(item, self.__head.next, self.__head)
self.__head.next.last = node
self.__head.next = node
self.__size += 1
# 删除
def removeHead(self):
node = self.__head.next
self.__head.next = node.next
node.next.last = self.__head
self.__size -= 1
del node
def removeTail(self):
node = self.__head.last
self.__head.last = node.last
node.last.next = self.__head
self.__size -= 1
del node
def size(self):
return self.__size
def isEmpty(self):
return self.__size == 0
dll = DoubleLoopLinked()
dll.insertToHead(1)
dll.insertToHead(2)
dll.insertToHead(3)
dll.insertToTail(4)
dll.insertToTail(5)
print(dll.size(),dll)
dll.removeHead()
dll.removeTail()
print(dll.size(),dll)
>>5 [3, 2, 1, 4, 5]
>>3 [2, 1, 4]