数组
数组Array是一种线性表数据结构,它使用一组连续的内存空间来存储一组具有相同类型的数据。
线性表:数据之间具有简单的前后关系,排列成线一样的结构,每个数据最多只有前后两个方向。数组、链表、队列、栈 都是线性表结构。
( 非线性表:数据之间不是简单的前后关系。树、图、堆等。)
查找
连续的内存空间:可以通过寻址公式计算出该元素存储的内存地址。
因此数组支持随机访问,根据下标随机访问的时间复杂度是O(1)
(不是所有查找时间复杂度都是O(1),只有根据下标是。二分查找复杂度是O(logN))
插入和删除
数组为了保持内存的连续性,会导致插入、删除操作相对比较低效。
插入:如果在末尾插入元素,就不需要移动数据,时间复杂度O(1);如果在开头插入元素,所有的数据都要依次向后移动一位,时间复杂度O(n);平均情况为O(n)
删除:和插入相似,删除第k个位置的数据,也需要移动数据保证内存的连续性。平均复杂度O(n)
异常
数组越界java.lang.ArrayIndexOutOfBoundsException
public static void main(String[] args) {
int[] arr = new int[3];
arr[3] = 5;
}
运行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at Test.main(Test.java:30)
原因是数组长度为3,数组下标从0开始,arr[3]已经不在arr数组范围内了。
容器与数组
容器类,例如:ArrayList
ArrayList的优势在于封装了许多数组操作的细节,例如插入删除方法,还可支持动态扩容。
ArrayList不能存储基本类型,如int、long等,需要使用包装类integer、Long等,而自动拆装箱也是有一定的性能消耗的,如果对性能消耗非常敏感,可以使用数组。
如果已知数据大小,且数据操作简单,可以使用数组。
其他
问:为什么大多数编程语言中,数组从0开始编号,而不是从1呢?
答:从存储内存模型来看,下标位置同时也包含了数组偏移的含义。
如果数组从0开始计数,a[0]就是数组的首地址位置,a[k]就是偏移k个元素大小的位置,即:a[k]_addr = base_addr + k * type_size;
如果数组从1开始计算,那么a[k]_addr = base_addr + (k-1)*type_size;
k-1对于cpu来说就增加了一道减法指令。
(也有可能是习惯问题,C语言设计就是从0开始的。)
数组的遍历
// for循环遍历
int[] arr = {1,2,3,4,5};
for(int i=0; i<arr.length; i++){
sout(arr[i]);
}
//TODO 迭代器遍历
Leecode
空
链表
上一节中表明了,数组是需要一块连续的内存空间来训相互的,堆内存要求较高。
而链表,则是通过“指针”讲一组零散的内存块关联起来。
单链表
其中我们将内存块成为链表的“结点(或节点)”,
而为了将结点串连起来,每个结点除了存储数据外还要记录下一个结点的地址,通常称记录下一个结点地址的指针为后继结点next。
头结点:第一个结点
尾结点:最后一个结点,尾结点中的next是一个空对象null,表示最后一个结点。
操作
链表支持数据的查找、插入、删除等操作。
链表的插入删除只需要考虑到相邻结点指针的改变,所以通常复杂度是O(1)。
而在查找的时候由于无法根据寻址进行随机访问,所以需要从头结点依次向下寻找,通常时间复杂度是O(n)。
循环链表
循环链表是在单链表的基础上,尾结点的后继结点指向了头结点,像环一样首尾相连,所以称作循环链表。
双向链表
单链表只有一个方向,也只有后继结点next指向后面的结点地址。而双向链表支持向前向后两个方向,除了后继结点next,还有前驱结点prev来指向前面的结点地址。
操作
当我们想要删除指定结点的前驱结点时,单链表由于只能向下寻找,所以为了找到前驱结点,我们需要从头遍历链表,直到p->next=q,说明p是q的前驱结点,而这个时间复杂度平均是O(n)。
但是对于双向链表来说,它直接指向了前驱结点,所以只需要p->prev就能得到p的前驱结点,时间复杂度是O(1)。
双向循环链表
在双向链表的基础上,头结点的前驱结点指向尾结点,尾结点的后继结点指向头结点。
基于链表实现LRU缓存淘汰算法
LRU-最近最少使用策略
Leecode
空

2万+

被折叠的 条评论
为什么被折叠?



