栈:后进先出
队列:先进先出
更复杂的队列:
1.优先队列
2.消息队列(队列中的元素带有类型,出队列的时候可以按照类型来取元素)
3.阻塞队列。如果队列满,此时继续插入元素,就会阻塞 如果队列空,此时继续取元素,也会阻塞
4.无锁队列,更高效的线程安全队列, 保证线程安全的常见手段,加锁,但是加锁效率比较低(CAS),加锁速度慢但是节省 CPU资源,CAS速度快,但是更消耗 CPU。
1 栈的实现:
核心操作是 入栈、出栈、取栈顶元素
1.1 顺序表实现栈:
使用尾插操作表示 “入栈”,使用尾删操作表示 “出栈”,使用根据下标获取元素的操作表示 “取栈顶元素”(头插头删,需要搬运之后的元素,时间复杂度会变高,变复杂);
public class MyStack_ArrayList {
private int[] data = new int[100];
private int size = 0;
// 1、入栈
public void push (int val) {
if (size > data.length) {
return;
}
data[size] = val;
size ++;
}
// 2、出栈
public Integer pop () {
if (size == 0) {
return null;
}
int ret = data[size - 1];
size --;
return ret;
}
// 3、取栈顶元素
public Integer peek() {
if (size == 0) {
return null;
}
return data[size - 1];
}
}
1.2 链表实现栈:
使用头插操作表示 “入栈”,使用头删操作表示 “出栈”,直接取头节点,就是 “取栈顶元素”(尾插尾删可以做到高效的实现,但是得记录下最后一个甚至倒数第二个元素的信息,代码更复杂);
class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public class MyStack2_LinkedList {
private ListNode head = null;
// 1、入栈
public void push(int val) {
ListNode newNode = new ListNode(val);
// 不带傀儡节点,就需要判断链表是否空
if (head == null) {
head = newNode;
return ;
}
newNode.next = head;
head = newNode;
}
// 2、出栈
public Integer pop () {
if (head == null) {
return null;
}
if (head.next == null) {
int ret = head.val;
head = null;
return ret;
}
int ret = head.val;
head = head.next;
return ret;
}
// 3、取栈顶元素
public Integer peek(){
if (head == null) {
return null;
}
return head.val;
}
}
2 队列的实现:
核心操作 出队列、入队列、取队首元素
2.1 顺序表实现队列:
Java标准库中的 Queue 对应的实现只有 LinkedList 一种选择,没有环形队列。
可以使用特殊的方式实现,用两个引用分别指着队的首部和尾部,有效数据的区间为 [ head , tail ),“入队列” 就把新的元素放到 tail 对应的下标上,同时 tail ++,“出队列” 就是把原来 head 指向的元素排除到有效区间之外,把 head ++(时间复杂度都为 O(1) )。
环形队列:
相比较于链表版本的队列,这个队列更快,但是空间是固定大小的,扩容成本很高。
(1) 队列为空,head 和 tail 重合;
(2)元素入队列,注意 tail 指向最后一个元素的后一个位置;
(3)接着元素入队列;
(4)队列满了,head 和 tail 重合;
这时会发现,队列满和队列空都是 head 和 tail 重合,我们就需要区分,什么时候是满了,什么时候是空。
方法一:
不把环形队列压榨的太干净,故意浪费一个空间,用 head 和 tail 重合表示空队列,用 tail == head - 1;表示满队列。
方法二:
通过多使用一个变量 size 来记录队列的元素个数,“size 等于 0” 就是空,“size == 数组长度” 就是满。
// 这里我们使用第二个方法来实现代码
// 用 size 维护代码为空还是为满
public class MyQueue2_ArrayList {
private int[] data = new int[100];
private int head = 0;
private int tail = 0;
int size = 0;
// 1、入队列
public boolean offer(int val) {
if (size == data.length) {
return false;
}
data[tail] = val;
tail ++;
if (tail == data.length) {
tail = 0;
}
size ++;
return true;
}
// 2、出队列
public Integer poll() {
if (size == 0) {
return null;
}
int ret = data[head];
head ++;
if (head == data.length) {
head = 0;
}
size --;
return ret;
}
// 3、取队首元素
public Integer peek() {
if (size == 0) {
return null;
}
return data[head];
}
}
2.2 链表实现队列:
使用尾插表示 “入队列”,使用头删表示 “出队列”,直接取到头结点表示 “取队首元素”;
package MyStackAndQueue;
public class MyQueue_LinkedList {
static class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
}
}
private ListNode head = null;
private ListNode tail = null;
// 1、入队列
public boolean offer(int val) {
ListNode newNode = new ListNode(val);
if (head == null) {
head = newNode;
tail = newNode;
return true;
}
tail.next = newNode;
tail = tail.next;
return true;
}
// 2、出队列
public Integer poll() {
if (head == null) {
return null;
}
if (head.next == null) {
int ret = head.val;
head = null;
return ret;
}
int ret = head.val;
head = head.next;
return ret;
}
// 3、取队首元素
public Integer peek() {
if (head == null) {
return null;
}
return head.val;
}
}
3 标准库中的 Stack / Queue
在标准库中,Stack 是一个类,继承自 Vector,Vector 和 顺序表、链表是并列关系,它们都实现了 List 接口;Queue 是一个接口,不能直接实例化,而需要创建对应的子类,实际使用的是 LinkedList 。
其中 Vector 现在使用的并不多,Vector 也是顺序表,和 ArrayList 很相似,它们的区别是:
(1) Vector 是线程安全的,ArrayList 是线程不安全的;
(2) Vector 扩容时,在典型的实现中,新的 capacity 是原来的 2 倍,而 ArrayList 是原来的 1.5 倍;
(3)Vector 出现时间较为久远,而 ArrayList 是之后才出现的。
还要注意,Stack 只有一套方法 push、pop 、peek 来完成核心的三个操作,而 Queue 有两套方法,一套方法返特殊值:offer、poll、peek;一套方法抛出异常:add、remove、element。