数组
定义数组变量: list = [] (以python举例)
可以是空数组,也可以直接存放初始值。当然也可以存放不同数据类型的元素(泛型)。相对高级一些。数组元素可以重复,集合不可以。(集合会自动给元素去重)
数组查询
数组创建时,会向内存申请一片连续的存储单元(开辟一串连续的存储空间),通过内存管理器来访问存储地址,查询元素。所以访问数组中的第一个元素和任意一个元素,时间和空间的复杂度都是一样的。常数级 O(1)。
我们通过下标(index)随机访问任何一个元素。可见访问时间非常之快。例如: arr[3];查找arr数组中index为3的元素。
增删数组元素(群移操作后且要检查上下界)
ABCEFG是一段连续的存储单元在内存空间中的元素,所谓连续地址是这样0x0001~0x0006,类似如此!
此刻插入元素D。 在index 2 的后面。那么需要提前将index 3~5 整体后移。占据内存空间index 4~6。 存储地址也随之改变(当然需要提前扩容,检查长度)。再把元素D插入进去。
如此可见,数组的插入操作,时间复杂度是O(n)。还是那句话:最坏的情况发生,才是时间复杂度的结果。假如把元素D插入到数组中第一个元素位置,index 为 0 的位置,则数组整体元素从A到G都要平移。所以这是最坏的结果。O(n)
删除也如此,需要移走元素D。 把E~G元素位置整体前移。最后修改下数组长度-1. 因为del一个元素。
链表数据结构(linked list)
链表的出现就是为了弥补数组的缺点: 增删不便。
链表是指物理存储空间上,不连续非顺序的存储结构,就是链表的元素位置不像数组那样呈连续的空间。数据项可以存在不同的内存地址上。
每个数据项都是一个node。每个node都是由data数据部分和指针构成的。每个node都可以通过next指针指向下一个node的位置。末尾的tail的next直接指向空数据项(None)。链表的表头由header表示。(参考下图)
上述是单链表,如果有prev node(前指针),就是双链表。(prev指针可以指向前一个节点node)。既有next,又存在prev node,所以双向链表。
若tail指向header就是循环链表。(环装蛇)
链表结构:表头,node节点和tail尾节点
链表的插入:(修改或者调整指针指向就完成了增删改)
调整前继节点next指针,指向插入新元素的节点位置,新元素的next指针指向之前的next node。
新增node节点(插入操作):
删除链表节点(删除操作):
删除节点是插入节点的逆操作。前驱节点的next直接指向删除节点的后一节点。
总结链表的增删查的时间复杂度:
链表的删除和增加操作,不需要引起群移操作,且不需要复制元素(挪元素)所以,时间复杂度是O(1)常数级。(只cut一个节点,在link一个新节点。两步走)
正因为如此,访问链表任意位置并不简单。除了访问head或者tail,是O(1)。若访问链表中间的任意元素,必须从head开始一步步往后移动,才能查到目标节点。所以时间复杂度是O(n)(线性N)
总结:链表除了lookup(随机访问)时间复杂度是O(n);其他增加删除节点都是O(1); 和数组正相反。所以,世界上没有完美的数据结构,都是互补的。完美是优秀的敌人。切记!
链表的优化:跳表(skip list)
对于有序数组,可以用二分查找来,如果有序的链表,如何加速呢?
这里科学家会引入跳表的概念。跳表的数据结构出现的很晚,它对标的是平衡树(AVL Tree)和二分查找。跳表无论插入,删除,搜索都是O(log n)的时间复杂度。参考下图
1. 跳表的使用只能用于有序的链表(链表元素有序)例如下方
2. 跳表可以加速链表的查询
跳表是通过增加多级索引的过程,类似于升维。 参考下图:比如找链表中的数字8. 首先从最高级索引查找,发现8>7. 所以,锁定下一级索引中的7-9之间。最后在原始表中找到8.
由此可见,第一级索引的步长相当于原始表的2个步长,节点数是n/2;第二级索引是原始表的4个步长,节点数是n/4。依次类推。第k级索引的个数就是 n/(2^k)。
时间复杂度计算: 总共有h级, 则最高级节点数为2. 那么n/2^h=2; 得到h = (log n)-1; log底数为2. 系数和常数不影响整体时间复杂度。(之前章节有介绍)。则跳表的时间复杂度为O(log n).
总结跳表:跳表将原始的时间复杂度为O(n)的链表,降到了log n, 大大改善了代码的运行效率。例如:原始普通链表有1024个元素,那么需要查询1024次才能完成的查询工作,用跳表log2(1024)=10, 此处2位底数. 只用了10次就完成了查找。
数组,链表,跳表时间复杂度对比总结:
本章小结:
跳表核心思想是链表的升维+空间换时间
数组和链表时间复杂度不同。 所以不同情况使用不同的数据结构。
链表和数组的增删改查方法,在大部分语言中都已经封装好了,可以直接使用,无需再次实现方法。但是实现的过程,还是需要结合代码了解一下。