数据结构与算法(Python)——常见数据结构Part1(Common data structures)
来源:互联网 发布:阿里云备案域名哪里买 编辑:程序博客网 时间:2024/04/27 16:40
写在前面
本节对常见数据结构做一个预览,我们的目的是快速了解他们,对于它们涉及到的复杂的数据结构和算法,在这里并不全部展开,留在后期详述。
1.数组
数组是我们要学习的第一个线性结构(Linear structure),所谓线性结构,指的是在数据有限集合中,每个数据元素都有一个确定的位置,例如
数组中存放的一般是同类型元素的集合,在物理存储上它们连续存放,通过索引来访问数组中的每个元素,数组可以是一维,二维和其他多维形式。
常见的一维数组形式表示为:
二维数组表示为:
访问一维数组我们只需要一个索引,而访问多维数组则需要多个多个数组组合的索引。例如存储(10, 20, 30, 40, 50)数据的数组表示为:
访问第一个元素,我们通过可以通过索引0表示为:numbers[0],它的值为10。
1.1 数组的特性
数组的实现可以分为固定大小的,也称之为静态数组(static array);另外一种是大小随着需要能够动态增长的,称之为动态数组(dynamic array)。静态数组,需要提前预知数组的大小,这种预判在某些情况下可能会偏大,从而导致存储空间的浪费,同时如果预估比实际需要少了,则容易发生数据溢出。动态数组可以根据需要动态的增长空间,内存安全的语言例如Java/C#之类的会自动管理内存,但是像C++中动态分配数组则需要用户自行管理,相比较而言容易导致错误。Python语言会自行分配和回收内存,数组天生就是动态数组。
在数组中访问元素,通过索引即可找到,时间复杂度为O(1)。
插入元素时,如果是尾部插入,则时间复杂度为O(1),如果是其他位置插入则需要移动其他元素,来保持数组的线性特性,假若数组长度为n,插入位置概率平均分布,则插入元素时移动元素的平均量为:
则平均时间复杂度为O(n)。同理,删除元素时,删除最后一个元素,则不需要移动元素,删除其余元素则需要移动其他元素,时间复杂为O(n)。
1.2 Python中的数组
在Python中,使用list来表示数组,list对象实现为一个动态数组,也就是长度根据需要自动调整的数组,这个数组支持不同类型元素存储。另外array模块也提供了接近C语言数组的只支持同类型元素的数组。
在Python中使用list存储上面数字:
numbers = [10, 20, 30, 40, 50] print("array length:", len(numbers)) print("first element:", numbers[0]) print("last element:", numbers[-1]) #('array length:', 5) #('first element:', 10) #('last element:', 50)
使用Array模块存储为:
import array # 使用l表示为int数组 numbers = array.array("l", [10, 20, 30, 40, 50]) print("array length:", len(numbers)) print("first element:", numbers[0]) print("last element:", numbers[-1])
Python中list支持多种操作,例如添加元素append,合并两个表使用extend操作,感兴趣地可以查看Python list。
1.3数组的应用
数组一般用来存放一组相关的数据,可以是简单的整数,也可以是复杂的对象;数组还可以同来实现其他数据结构,例如使用数组实现栈、队列等数据结构。
2.链表
链表是我们要学习的第二种线性结构。与数组不同的时,链表中每个元素也称之为节点(Node),它的物理位置并不一定与它的前驱或者后继相邻,可以在其他任意位置,只要有一种线索将每个元素串起来,形成这个线性结构即可。这个线索,就是每个节点的地址,习惯上称之为指针。
2.1 不同类型的链表
例如如下图所示为有一个指向后继节点指针的单链表(single linked list):
单链表中有一个头指针(Head)指向链表中第一个节点,最后一个节点没有后继节点了,因此它的指针指向空即NULL。
通过上面的观察,我们可以得出单链表对应的节点结构,可以表达为:
class SingleLinkListNode(object): """ 单链表结点类 """ def __init__(self, data=None, next_node=None): self.data = data # 数据域 self.next = next_node # 指向下一个结点的指针(也即是地址) def __str__(self): return str(self.data)
在上述单链表中,如果我们已经找到了第i个节点,能不能找到位于i之前的节点呢? 这里需要在尾部节点上做一个修改,使它的指针指向链表头,这样构成了一个环形结构,这样的链表称之为循环链表(Circular linked list)。在循环链表中,可以通过任意节点找到链表中的其他节点。
另外一个问题是,如果我们已经找到了第i个节点,想知道它的前驱怎么办?我们可以在遍历过程中,用一个临时变量记住它的前驱节点。另一种方法是,在节点结构中提供两个指针,一个指向后继,一个指向前驱,这种链表称之为双向链表(Double linked list),它的节点结构声明为:
class DoubleLinkListNode(object): """ 双链表结点类 """ def __init__(self, data=None, prev_node=None,next_node=None): self.data = data # 数据域 self.prev = prev_node # 指向前驱节点 self.next = next_node # 指向后继节点 def __str__(self): return str(self.data)
当然也可以在双链表中,加入循环机制,构成循环双链表。上述3种链表,对比如下图所示:
2.2 链表特性
以单链表为例进行说明。
在上面的链表中,我们可以看出,如果需要访问数组中第i个元素,则需要从头开始遍历链表,除此之外别无它法(花费额外空间保存第i个元素地址除外),因此链表中访问元素时间复杂度为O(n)。
插入元素时,头部插入则仅仅需要重新调整头指针和插入节点的指针,时间复杂度为O(1)。尾部插入时,首先需要找到尾部节点,这个遍历过程时间复杂度为O(n),插入时间复杂度为O(1),因此总的时间复杂度为O(n);注意,如果链表中保存了尾部指针位置,则尾部插入时时间复杂度也能达到O(1)。
删除元素时,我们首先需要定位到指定节点,然后删除,则时间复杂度为O(n)。
2.3 Python中链表
Python中并没有提供链表的实现,但是list结构本身是支持动态增长的数组,且数组元素是引用类型,因此使用list类型即可满足编程需求。这里自己动手实现一个上面描述的单链表:
class LinkListNode(object): """ 链表结点 """ def __init__(self, data=None, next_node=None): self.data = data self.next = next_node def __str__(self): return str(self.data)class LinkList(object): """ 单链表类 """ def __init__(self): self.head = LinkListNode() # 表头的空结点指针 Dummy node self.tail = self.head def __init__(self, data_array): if type(data_array) is not list: raise ValueError("init with data array only.") self.head = LinkListNode() cur_node = self.head for x in data_array: cur_node.next = LinkListNode(x) cur_node = cur_node.next self.tail = cur_node def __str__(self): head = self.head.next output_str = "LinkList[" while head: output_str += str(head.data)+", " head = head.next output_str += "]" return output_str def push_back(self, data): self.tail.next = LinkListNode(data) self.tail = self.tail.next def back(self): if self.tail: return self.tail.data else: return None def pop_back(self): if self.tail != self.head: node_to_remove = self.head.next prev_node = self.head while node_to_remove and node_to_remove.next: prev_node = node_to_remove node_to_remove = node_to_remove.next prev_node.next = None self.tail = prev_node del node_to_remove def front(self): if self.head.next: return self.head.next.data else: return None def push_front(self, data): self.head.next = LinkListNode(data, self.head.next) if self.tail == self.head: self.tail = self.head.next def pop_front(self): if self.head != self.tail: first_node = self.head.next self.head.next = first_node.next if not self.head.next: self.tail = self.head del first_node def remove_at(self, index): if index < 0 or self.tail == self.head: return False node_to_remove = self.head.next prev_node = self.head for i in range(index): prev_node = node_to_remove node_to_remove = node_to_remove.next if not node_to_remove: return False prev_node.next = node_to_remove.next if not prev_node.next: self.tail = prev_node del node_to_remove return True def is_empty(self): return self.tail == self.head def size(self): count = 0 cur_node = self.head.next while cur_node: count += 1 cur_node = cur_node.next return count @staticmethod def print_list(list_head): """ 打印链表 :param list_head: 链表头结点 :return:None """ output_str = str(list_head) print(output_str)
注意上述实现的几点:
提供了一个head指向一个空的头结点,一个tail指针指向尾部节点。
提供了额外的tail指针,虽然方便了尾部插入;但使得在进行某些操作时,总是得考虑尾部指针的更新,例如删除节点时如果删除了尾部节点则应更新tail。可见增加一个指针成员将使得编码变得复杂了。
上面实现中提供了两个初始化方法,可以构建一个空的链表,也可以从一个数组构造链表。
上述实现中,提供了类的静态打印数据方法,这个方法重写了
__str__
方法。
为上述单链表类编写一个测试用例:
if __name__ == "__main__": link_list = LinkList([1, 2, 3, 4]) LinkList.print_list(link_list) i = 0 while not link_list.is_empty(): if i % 2: link_list.pop_back() else: link_list.pop_front() i += 1 LinkList.print_list(link_list)
输出:
LinkList[1, 2, 3, 4, ]LinkList[2, 3, 4, ]LinkList[2, 3, ]LinkList[3, ]LinkList[]
链表实现的时候,测试用例的要考虑的情形还是挺多的,在编写的时候尤其要考虑到特殊情况,例如删除空表中元素等。双链表的实现可以自行实现,也可以参考Linked Lists in Python。
3.数组和链表的比较
数组的实现要比链表简单,但是为什么还是要引入链表呢?通过下面这个表格的对比,我们就能回答这个问题(整理自Linked Lists)。
总结一下链表的优势在于:
- 1)不用提前估算空间,空间也不需要预分配
- 2)在删除结点和添加结点时比数组快(删除元素时,数组需要移动元素,这开销远比链表遍历指针大,虽然二者复杂度都为O(n))。
链表的劣势在于:
- 1)不能随机访问,除了头尾结点外只能顺序遍历。
- 2) 存储指针也将带来额外开销。
数组的优势在于:支持随机访问 可以使用索引访问;劣势在于要求实现预分配空间,通常分配的空间比使用的多,而且在内存碎片情况下,可能无法继续扩大。
关于两者的比较,感兴趣地还可以参考:
- 1)Difference between Array and Linked List
- 2)SO
- 3)geeksforgeeks
4. Python中list实现说明
Python中list实现为动态数组,而不是链表,可以参考其结构定义:
typedef struct { PyObject_HEAD Py_ssize_t ob_size; /* Vector of pointers to list elements. list[0] is ob_item[0], etc. */ PyObject **ob_item; /* ob_item contains space for 'allocated' elements. The number * currently in use is ob_size. * Invariants: * 0 <= ob_size <= allocated * len(list) == ob_size * ob_item == NULL implies ob_size == allocated == 0 */ Py_ssize_t allocated;} PyListObject;
这是一个预分配的动态数组,真实大小为ob_size,分配大小为allocated,其中ob_item是一个指针数组,每个元素指向PyObject对象。感兴趣地可以参考SO。
也可以通过下面的图来理解Python中list的实现:
本部分介绍了常见数据结构中的数组和链表,要想较好地掌握本节内容,更多的练习必不可少,这里推荐leetcode linkedlist训练的习题。
- 数据结构与算法(Python)——常见数据结构Part1(Common data structures)
- 数据结构与算法(Python)——常见数据结构Part2(Common data structures)
- 数据结构与算法(Python)——常见数据结构Part3(Common data structures)
- Algorithms.and.Data.Structures算法与数据结构
- 数据结构与算法的学习问题——The Data Structures and Algorithms Learning Problem
- Python数据结构与算法分析学习记录(1)——基于Problem Solving with Algorithms and Data Structures using Python的学习
- Python数据结构与算法分析学习记录(2)——基于Problem Solving with Algorithms and Data Structures using Python的学习
- 数据结构与算法 Data Structures and Algorithms(作业1)
- 数据结构 Data Structures
- Java数据结构(Data Structures)
- 常见数据结构与算法的 Python 实现
- Data Structures and Algorithm Analysis in C, Second Edition(《数据结构与算法分析》C语言版 第二版)——Mark Allen Weiss
- Data Structures and Algorithm Analysis in C (数据结构与算法分析) 读书总结
- Python 数据结构与算法——递归
- Python 数据结构与算法——deque
- Python 数据结构与算法 —— 链表
- Python 数据结构与算法 —— 哈弗曼树
- Python数据结构与算法
- 画图(分形)
- 报名 | 第二届全国智能制造(中国制造2025)创新创业大赛总决赛将在京举行!
- 闭锁电源开关使用瞬时按钮---凯利讯半导体
- 《深入理解java虚拟机》读书笔记
- 电子世界里的一点感想
- 数据结构与算法(Python)——常见数据结构Part1(Common data structures)
- python批量重命名文件方法
- 吴恩达深度学习课程deeplearning.ai课程作业:Class 4 Week 1 Convolutional Neural Networks: Step by Step
- Ubuntu16.04循环登录问题
- [Leetcode] 521. Longest Uncommon Subsequence I 解题报告
- 常见开放数据集
- VS2013 无法找到资源编译器DLL
- [知了堂学习笔记]_JQuery选择器
- Firefox用户,一个不小心你们的密码可能已经被泄露