声明:本文主要作为作者的复习笔记,由于作者水平有限,难免有错误和不准确之处,欢迎读者批评指正。
目录快捷跳转
- 1. 线性表接口两个常用子类
- 2. 什么时候选择ArrayList,什么时候选择LinkedList?
- 3. 栈和队列的关系
- 4. 栈
- 5. 栈的实现根据使用的内部结构不同分为顺序栈和链式栈
- 6. 栈、虚拟机栈、栈帧有什么区别
- 7. 队列(FIFO:First In First Out)
- 8. 广义上的队列一般有以下几种
- 9. 循环队列
- 10. 循环队列判断是否为空,判断队列是否已满
- 11. 无论是入队还是出队,在循环队列中,下标后移都需要进行取模操作
- 12. 循环队列不断入队和出队中,有效元素以及浪费的空间都在动态变化
- 13. 双端队列(Deque)
- 14. 常见的线性表:元素之间呈线性排列,一次只能添加一个同类型的元素;
1. 线性表接口两个常用子类
ArrayList => 底层是基于动态数组的实现
LinkedList => 底层是基于双向链表的实现
- LinkedList是队列Queue的子类,还是双端队列Deque的子类;
- LinkedList有一个反向遍历的迭代器,从链表的尾部开始遍历;也有一个正向的迭代器,从链表的头部开始遍历。
- 使用List接口的方法都一样,都是List接口中定义好的方法;
2. 什么时候选择ArrayList,什么时候选择LinkedList?
不同点 | ArrayList | LinkedList |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持;O(1) | 不支持;O(N) |
头插 | 需要搬移元素,效率低O(N) | 只需修改引用的指向,时间复杂度为O(1) |
插入 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除(频繁) |
若ArrayList和LinkedList都默认调用add方法进行插入,这两个结构都在进行尾插;其实数组的尾插比链表还快elementData[size++] = val;
3. 栈和队列的关系
栈和队列都是添加和删除元素操作受限的线性表,本质上栈和队列没什么不同,可以相互转换,都是线性表的一种;
栈:树型结构的dfs(深度优先遍历 - 前中后序)
队列:树型结构的bfs(广度优先遍历 - 层序遍历)
应用场景
可以使用栈来模拟队列,同样的,也可以使用队列来模拟栈的实现;
4. 栈
一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
- 入栈(push)
栈的插入操作叫做进栈/压栈/入栈;入数据在栈顶。 - 出栈(pop)
栈的删除操作叫做出栈;出数据在栈顶。 - 无论入栈还是出栈,都只能固定在栈顶进行。
- 查看栈顶元素peek():只取值,不删除。
5. 栈的实现根据使用的内部结构不同分为顺序栈和链式栈
- 顺序栈:内部还是动态数组,规定只能从数组的末尾进行插入和删除;
插入 => 入栈
删除 => 出栈 - 链式栈:内部使用链表,默认规定在链表的尾部进行插入/删除;
6. 栈、虚拟机栈、栈帧有什么区别
- 栈数据结构的栈:线性表的一种,满足后进先出的特点(LIFO),保存在栈中的元素都是相同数据类型;
- 虚拟机栈,操作系统栈:概念性的东西,底层到底是不是用的栈(数据结构),到底用的链式栈还是顺序栈都不是关键;
- 栈帧:函数从调用过程到结束的一个体现,一个函数从调用到销毁其中占用的空间,内部的局部变量都统一放在该函数的栈帧中(一种类型,专门描述函数在操作系统内部的调用过程);
7. 队列(FIFO:First In First Out)
操作受限的线性表,只能从队列的一端添加元素(队尾),从队列的另一端删除元素(队首);
- 入队:向队列中添加元素(offer | push);
- 出队:从队列中移除元素(poll);
8. 广义上的队列一般有以下几种
- 普通队列(FIFO链式队列)— 基于链表实现(单链表足够,尾插头删);
- 循环队列,可以从队尾走到队首,使用定长数组来实现;
- 优先级队列(非线性结构),一般都是先进先出,但是,优先级较高的元素可以优先出队;内部使用堆这个数据结构来实现(JDK默认使用的是最小堆 — 完全二叉树);
9. 循环队列
循环队列使用定长数组来实现,数组的首尾相连就构成了循环队列(就是走到数组最后一个元素时,下一个访问的就是数组的首地址);循环队列一般用在os生存消费者模型。
10. 循环队列判断是否为空,判断队列是否已满
判断循环队列是否为空:head == tail
判断循环队列是否已满:(tail + 1)% num.length == head;
- head索引指向队列队首第一个元素的下标,tail索引指向队列最后一个元素的下一个位置下标(新元素就可以使用tail直接入队列);
- 无论循环队列是空还是已满,head == tail重合了;因此为了区分循环队列到底为空还是已满,循环队列会浪费一个空间;
例:要保存一个最大支持5个元素的循环队列,则开辟一个长度为6的数组!多余的这个空间就是来判断队列是否已满。
11. 无论是入队还是出队,在循环队列中,下标后移都需要进行取模操作
- 入队:offer
tail继续向后走,tail = (tail + 1)% num.length; - 出队:poll
head继续向后走,head = (head + 1)% num.length;
12. 循环队列不断入队和出队中,有效元素以及浪费的空间都在动态变化
在循环队列中,浪费空间的位置不是固定的,每次(tail + 1)% num.length到head之前的那个空间是浪费的。
在任意时刻
int lastIndex = tail == 0 ?num.length - 1 : tail - 1;
队列中的有效元素 [head…lastIndex];
13. 双端队列(Deque)
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque是“double ended queue”的简称;
- 元素可以从队头出队和入队,也可以从队尾出队和入队;
- 在Java中,只要用栈和队列,统一使用Deque接口!根据实际使用时的场景选择基于数组实现的ArrayDeque或者基于双向链表实现的LinkedList;
Deque接口的使用
//使用Deque作为栈,方法名都不变 push pop peek
Deque<Integer> stack = new LinkedList<>();
stack.push(1);
stack.push(3);
stack.push(5);
stack.push(7);
System.out.println(stack);
//输出7
System.out.println(stack.pop());
//使用Deque作为队列,方法名不变 offer poll peek
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(2);
queue.offer(4);
queue.offer(6);
queue.offer(8);
System.out.println(queue);
//输出2
System.out.println(queue.poll());
14. 常见的线性表:元素之间呈线性排列,一次只能添加一个同类型的元素;
数组,链表,栈,队列,字符串(char[ ] 多个字符呈线性排列);