Java数据结构之线性表、栈和队列

线性表

线性表详解:数据结构线性表10分钟入门建议笔记结合这个教程一起看

线性表,全名为线性存储结构。将具有一对一关系的数据线性地存储到物理空间中,这种存储结构就称为线性存储结构(简称线性表)。

线性表常用术语:

数据结构中,一组数据中的每个个体被称为“数据元素”(简称“元素”)。

另外,对于具有“一对一”逻辑关系的数据,我们一直在用“某一元素的左侧(前边)或右侧(后边)”这样不专业的词,其实线性表中有更准确的术语:

  • 某一元素的左侧相邻元素称为“直接前驱”,位于此元素左侧的所有元素都统称为“前驱元素”
  • 某一元素的右侧相邻元素称为“直接后继”,位于此元素右侧的所有元素都统称为“后继元素”

线性表又分为顺序表链表

  1. 将数据依次存储在连续的整块物理空间中,这种存储结构称为顺序存储结构(简称顺序表
  2. 数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构(简称链表

Java Collection API中的表

在类库中,Java 语言包含有一些普通数据结构的实现,该语言的这一部分通常叫作Collections API。表ADT是在Collections API中实现的数据结构之一。我们会在后面的笔记中看到其他一些数据结构。

Collection接口:

Collections API位于java.util包中。集合( collection)的概念在Collection接口中得到抽象,它存储一组类型相同的对象。在Collection接口中的许多方法所做的工作由它们的英文名称可以看出:如size()返回集合中的项数; isEmpty()返回true当且仅当集合的大小为0。如果x在集合中,则contains()返回true。注意,这个接口并不规定集合如何决定x是否属于该集合一,这要由实现该Collection接口的具体的类来确定。add和remove从集合中添加和删除x,如果操作成功则返回true,如果因某个看似有理(非异常)的原因失败则返false。例如,如果要删除的项不在集合中,则remove可能失败,而如果特定的集合不允许重复,那么当企图插人一项重复项时,add操作就可能失败。(具体可查看jdk1.8文档)

Collection接口扩展了Iterable接口实现Iterable接口的那些类可以拥有增强的for循环,该循环施于这些类之上以观察它们所有的项。

Iterator接口:

实现Iterable接口的集合必须提供一个称为iterator的方法,该方法返回一个Iterator类型的对象。该Iterator是一个在java.util包中定义的接口。(next() 返回迭代中的下一个元素,hasNext() 返回 true如果迭代具有更多的元素,即可以继续迭代)

重要的一个基本法则:当直接使用Iterator(而不是通过一个增强的for循环间接使用)时,如果对正在被迭代的集合进行结构上的改变(即对该集合使用add、remove或clear方法),那么迭代器就不再合法(并且在其后使用该迭代器时将会有ConcurrentModi ficat ionException异常被抛出)

List接口、ArrayList类和LinkedList类

List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList。List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。3个具体实现类的相关区别如下:

  1. ArrayList是最常用的List实现类,内部是通过数组实现的(顺序表),它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除
  2. Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
  3. LinkedList是用链表结构存储数据的(链表),很适合数据的动态插入和删除随机访问和遍历速度比较慢。另外,它还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

List接口继承了Collection接口,具体的一些方法可查看jdk1.8文档。

List ADT有两种流行的实现方式。ArrayList类提供了List ADT的一种可增长数组的实现(顺序表)。使用ArrayList的优点在于,对get和set的调用花费常数时间。其缺点是新项的插人和现有项的删除代价昂贵,除非变动是在ArrayList的末端进行。LinkedList类则提供了List ADT的双链表实现(链表)。使用LinkedList的优点在于,新项的插人和现有项的删除均开销很小,这里假设变动项的位置是已知的。这意味着,在表的前端进行添加和删除都是常数时间的操作,由此LinkedList 更提供了方法addFirst和removeFirst、 addLast 和removeLast、以及getFirst和getLast等以有效地添加、删除和访问表两端的项。使用LinkedList的缺点是它不容易作索引,因此对get的调用是昂贵的,除非调用非常接近表的端点(如果对get的调用是对接近表后部的项进行,那么搜索的进行可以从表的后部开始)。

ArrayList的实现

ArrayList的实现,即顺序表的实现。这里我们将类命名为MyArrayList,用于区分ArrayList。

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public class MyArrayList<E>{
private static final int DEFAULT_CAPACITY = 10; // 默认容量
private int theSize; // 数组长度
private E [ ] theItems; // 用数组来实现MyArrayList

/**
* 构造方法,构造一个MyArrayList(每次构造前执行一次doClear方法)
*/
public MyArrayList() {
doClear();
}

/**
* 清除MyArrayList
*/
public void clear( )
{
doClear( );
}

/**
* 返回MyArrayList集合中元素个数
* @return 返回集合中元素个数
*/
public int size(){
return theSize;
}

/**
* 如果MyArrayList为空,返回true
* @return 如果集合为空,返回true
*/
public boolean isEmpty(){
return size()==0;
}

/**
* 返回MyArrayList中索引为index的元素
* @param index 集合索引
* @return 返回集合中索引为index的元素
* @throws ArrayIndexOutOfBoundsException 若索引不合法或超出范围,抛出错误
*/
public E get(int index){
if( index < 0 || index >= size( ) )
throw new ArrayIndexOutOfBoundsException( "Index " + index + "; size " + size( ) );
return theItems[index];
}

/**
* 将MyArrayList中索引为index的值设置为newValue
* @param index 索引值
* @param newValue 要设置的新值
* @return 集合中该索引原来的值(被替换的值)
* @throws ArrayIndexOutOfBoundsException 若索引不合法或超出范围,抛出错误
*/
public E set(int index, E newValue){
if( index < 0 || index >= size( ) )
throw new ArrayIndexOutOfBoundsException( "Index " + index + "; size " + size( ) );
E oldValue = theItems[index];
theItems[index] = newValue;
return oldValue;
}

/**
* 在MyArrayList的指定处添加一个元素value
* @param index 索引值
* @param value 添加的元素值
*/
public void add(int index, E value) {
if (theItems.length==size()){ // 如果长度不够,则需要扩展然后再add元素
ensureCapacity(size()*2+1);
}
// 注意这里的处理逻辑,很精妙。画个图就能懂
for (int i = theSize; i > index; i--) {
theItems[i] = theItems[i-1];
}
theItems[index] = value;
theSize++; // 添加后别忘了theSize+1
}

/**
* 向MyArrayList的末尾添加一个元素
* @param value 要添加的元素
* @return 添加成功,返回true
*/
public boolean add(E value){
add(size(), value);
return true;
}

/**
* 从MyArrayList中删除指定索引处的元素
* @param index 索引
* @return 返回被删除的元素
*/
public E remove(int index){
E removedValue = theItems[index];
// 注意一下这里删除的逻辑
for (int i = index; i < size() - 1; i++) {
theItems[i] = theItems[i+1];
}
theSize--; // 删除后别忘了theSize-1
return removedValue;
}

/**
* 扩充MyArrayList长度
* @param newCapacity 扩充后的长度
*/
@SuppressWarnings("unchecked") // 告诉编译器忽略指定的警告,不用在编译完成后出现警告信息。
public void ensureCapacity(int newCapacity) {
if( newCapacity < theSize )
return;

E[] old = theItems;
theItems = (E[]) new Object[newCapacity];
for (int i = 0; i < size(); i++) {
theItems[i] = old[i];
}
}

/**
* 重写toString方法
* @return
*/
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[ ");
for (int i = 0; i < size(); i++) {
stringBuilder.append(theItems[i]+" ");
}
stringBuilder.append("]");
return new String(stringBuilder);
}

private void doClear() {
theSize = 0;
ensureCapacity(DEFAULT_CAPACITY);
}
}

LinkedList的实现

链表(链式存储结构)及创建

Java实现单向链表基本功能

LinkedList的实现,即链表的实现。这里我们将类命名为MyLinkedList,用于区分LinkedList。这里我们先写一个单链表

链表又分为几类:

  • 单向链表:一个节点指向下一个节点
  • 双向链表:一个节点有两个指针域(指向前一个和下一个)
  • 循环链表:能通过任何一个节点找到其他所有的节点,将两种(单/双)链表的最后一个节点指向第一个节点从而实现循环。

操作链表时要时刻记住:节点中指针域指向的就是下一个节点

链表中的节点又细分为:头节点,首元节点和其他节点

  • 头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题
  • 首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义
  • 其他节点:链表中其他的节点

相关算法都在代码注释中,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import java.util.HashSet;
import java.util.Set;

public class MyLinkedList<E> {
/**
* 定义头节点head
*/
private Node head = new Node();

/**
* 节点类
* 每个节点包括:数据域和指针域
*/
private class Node {
public E data; // 数据域
public Node next; // 指针域,指向下一个节点

public Node() {
this.data = null;
this.next = null;
}

public Node(E data) {
this.data = data;
}

public Node(E data, Node next) {
this.data = data;
this.next = next;
}

}

/**
* 在单链表的末尾添加一个数据
* @param value 要添加的数据
*/
public void addData(E value){
Node addNode = new Node(value); // 要插入的节点
Node temp = head; // 临时节点,从头节点开始
while (temp.next != null){ // 找到尾节点,循环结束后尾节点即为temp
temp = temp.next;
}
temp.next = addNode; // 在尾节点后面添加节点
}

/**
* 在第index个节点处插入新的元素
* @param index 要在第index位置后面插入
* @param value 插入的值
*/
public void insertData(int index, E value){
if (index<1 || index>length()+1){ // 首先判断插入位置是否合法
System.out.println("插入位置不合法!");
return;
}
Node temp = head.next; // 临时节点,从首元节点开始
int current = 1; // 记录遍历过程中的当前位置(第几个)
Node insertNode = new Node(value); // 初始化要插入的节点
while (temp.next != null){
if (current == index){ // 找到要插入的节点了(即在第index个节点出插入)
insertNode.next = temp.next; // 新的节点指向原来(第index个节点)指向的下一个节点
temp.next = insertNode; // 将第index个节点指向的下一个节点变成我们要插入的节点
return;
}
current++;
temp = temp.next;
}
}

/**
* 获取单链表长度
* @return 单链表长度
*/
public int length(){
int length = 0;
Node temp = head.next; // 临时节点,从首元节点开始
while (temp!=null){ // 找到尾节点
temp = temp.next;
length++;
}
return length; // 很显然长度是从首元结点开始算的,而不是头结点head
}

/**
* 根据位置删除节点
* @param index 删除第index处的节点
*/
public void deleteByIndex(int index){
if (index<1 || index>length()+1){ // 首先判断删除位置是否合法
System.out.println("插入位置不合法!");
return;
}
Node temp = head; // 临时节点,从头节点开始
int current = 1; // 记录遍历过程中的当前位置(第几个)
while (temp.next!=null){
if (current == index){ // 找到要删除的节点的前一个节点(即temp为要删除的节点的前一个节点)
Node deleteNode = temp.next;
temp.next = deleteNode.next; // temp节点指向原本被删除的节点(deleteNode)指向的下一个节点
// deleteNode = null; (Java会回收它,不需要我们设置)
return;
}
current++;
temp = temp.next;
}
}

/**
* 返回链表中倒数第k个元素
* 这个算法很有意思:设置两个指针p1,p2,让p2比p1快k-1个节点,这样当p2为终点时,p1即为倒数第k个节点
* @param k 找到链表中倒数第k个元素
* @return
*/
public E findData(int k){
if (k<1 || k>length()){
return null;
}
// 设置两个指针,都从首元节点开始
Node p1 = head.next;
Node p2 = head.next;
// 令p2比p1快k-1个节点
for (int i = 0; i < k-1; i++) {
p2 = p2.next;
}
/* 只要p2.next为null,p2就指向最后一个元素了,
p2比p1快k-1个节点,p2为最后一个节点,则p1为倒数第k个节点*/
while (p2.next != null){
p1 = p1.next;
p2 = p2.next;
}
return p1.data;
}

/**
* 删除链表中的重复元素(用哈希表的方式)
*/
public void deleteDuplicates(){
// 定义一个哈希表,HashSet的底层是HashMap
Set<E> hashSet = new HashSet<>();
Node temp = head.next; // 临时节点,从首元节点开始
if (temp==null){
return;
}
hashSet.add(temp.data); // 链表的第一个元素(即首元节点)肯定是不重复的
while (temp != null){
/* 判断temp节点的下一个节点是否重复
* 注意这里一定要加一个temp.next!=null的条件 */
while (temp.next!=null && hashSet.contains(temp.next.data)){
temp.next = temp.next.next; // 删除temp的下一个节点
}
/* 这里的if也要单独提取出来,如果把hashSet.add(temp.next.data)
* 语句放在上面的while循环(循环条件也有temp.next != null)中,会报空指针异常*/
if (temp.next != null){
hashSet.add(temp.next.data);
}
temp = temp.next;
}
}

/**
* 查询链表的中间的元素
* 在链表长度未知的情况下,这里的算法也挺有意思的
* 设置两个指针p1和p2,p1走一步,p2走两步,直到p2走到终点(长度为奇数)或倒数第2个节点(长度为偶数)
* 其中的逻辑画个图就能理解了
* @return 链表中间节点的data
*/
public E searchMid(){
/**
* 在未知链表长度的情况下
*/
// 设置两个指针,都从首元节点开始
Node p1 = head.next;
Node p2 = head.next;
/* p1节点走一步,p2节点走两步,当p2节点为终点时,p1为中间节点
* 其实画个图就能理解这个逻辑,我们的条件设置为p2!=null && p2.next!=null && p2.next.next!=null
* 当节点数为奇数时,p2能够走到最后一个节点
* 当节点数为偶数时,p2只能走到倒数第2个节点,所以p1为中间偏左的节点*/
while (p2!=null && p2.next!=null && p2.next.next!=null){
p1 = p1.next; // p1走一步
p2 = p2.next.next; // p2走两步
}
return p1.data;

/**
* 已知链表长度就很简单了
*/
// int count = length()/2;
// Node temp = head.next;
// for (int i = 0; i < count; i++) {
// temp = temp.next;
// }
// return temp.data;
}

/**
* 从尾到头输出整个链表,
*/
public void printListReversely(){
printListReversely(head);
}
/**
* 从node结点下一个结点开始,从尾到头反转输出链表。通过递归实现
* 即从尾到头反转输出链表的最后一个元素为node结点的下一个结点
* @param node
*/
public void printListReversely(Node node){
if (node!=null){ // 递归逻辑画下图就能理解(栈),注意head节点为头结点而不是首元结点
printListReversely(node.next);
if (node.data!=null){
System.out.print(node.data+" ");
}
}
}

/**
* 反转整个链表
*/
public void reverseList(){
// 这里我们的头结点一直没变,将头结点指向原链表的最后一个结点
// reverseList(head.next)会将头结点之后的所有结点反转,然后返回反转后链表的首元结点
// 这时我们将原有的头结点指向该方法返回的结点即可
// 说的有点绕但是画一下图就能理解
head.next = reverseList(head.next); // 注意这里的参数是首元结点而不是头结点!
}
/**
* 以node结点开始,反转链表
* 注意是以node结点开始,即反转后node结点就是新链表的最后一个结点
* @param node
* @return
*/
public Node reverseList(Node node){
Node pre = null;
Node cur = node;
while (cur!=null){ // 这里通过非递归实现,逻辑有点绕需要画图理解,但其实画一下图也很好理解
Node next = cur.next;
cur.next = pre; // 这里是修改结点指向的操作
pre = cur;
cur = next;
}
return pre;
}

/**
* 重写toString方法,结果要遍历链表并输出
* @return
*/
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[ ");
Node temp = head.next; // 临时节点(遍历从首元结点开始)
while (temp!=null){ // 注意:这里一定要先判断temp是否为null,不能直接while (temp.data!=null)
if (temp.data!=null){
stringBuilder.append(temp.data+" ");
}
temp = temp.next; // 继续下一个
}
stringBuilder.append("]");
return new String(stringBuilder);
}
}

反转整个链表的实现逻辑光看代码可能有点麻烦,这里贴一下比较潦草的草稿:

循环链表实现约瑟夫环

约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.util.Scanner;

/**
* 用循环链表实现约瑟夫环
*/
public class Main {

/**
* 单链表中的节点类
*/
private static class Node{
int value;
Node next;

public Node(int value) {
this.value = value;
}
}

public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入 n 个人围坐在一张圆桌周围的n值:");
int n = scanner.nextInt();
System.out.print("请输入数到m的那个人出列的m值:");
int m = scanner.nextInt();
System.out.print("请输入从编号为k的人开始顺时针报数的k值:");
int k = scanner.nextInt();
/* 头节点单列出来,方便形成循环链表 */
Node head = new Node(1);
Node temp = head;
/* 建立单链表 */
for (int i = 2; i <= n; i++) { // 头节点已经提前创建了,只需要循环n-1次(i从2开始,小于等于n)
temp.next = new Node(i);
temp = temp.next;
}
/* 循环结束后,让最后一个节点temp指向首节点,完成循环链表 */
temp.next = head;
/* 从k开始数数,所以temp要为k-1个编号出的节点 */
for (int i = 1; i < k; i++) {
temp = temp.next;
}
/* 注意此时temp节点是循环链表中的最后一个节点 */
while (temp != temp.next){ // 即循环链表长度不为1
/* for循环结束后,temp节点从跳m-1次后,到达要删除节点的前一个节点 */
for (int i = 1; i < m; i++) { // 数m次,节点跳m-1次(如数3次,节点跳两次,从1跳到3)
temp = temp.next;
}
/* 数完后,输出出列的序号,并删除temp节点的下一个节点(即数到要出列的节点) */
System.out.println("出列人的编号为:"+temp.next.value);
temp.next = temp.next.next; // 删除temp节点的下一个节点
}
/* while循环结束后,循环链表只剩下最后一个节点temp*/
System.out.println("最后胜利的编号为:"+temp.value);
}
}

什么是栈,栈及其特点和应用详解

栈是重要的数据结构,从数据结构角度看,栈也是线性表,其特殊性在栈的基本操作是线性表的子集。Stack作为最基本的数据结构,在JDK代码中,也有它的实现,java.util.Stack类是继承了Vector类,来实现了栈的基本功能。

栈的特点:

  • 栈只能从表的一端存取数据,另一端是封闭的
  • 在栈中,无论是存数据还是取数据,都必须遵循"先进后出"的原则,即最先进栈的元素最后出栈

基于栈结构的特点,在实际应用中,通常只会对栈执行以下两种操作:

  • 向栈中添加元素,此过程被称为"进栈"(入栈或压栈)
  • 从栈中提取出指定元素,此过程被称为"出栈"(或弹栈)

栈的实现

由于栈是一个表,因此任何实现表的方法都能实现栈。显然,ArrayList和LinkedList都支持操作。这里我们就只给出用ArrayList实现栈的代码了,很简单的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.ArrayList;
import java.util.EmptyStackException;

public class MyStack<E> {
private ArrayList<E> myStack;

public MyStack() {
myStack = new ArrayList<E>();
}

/**
* 入栈
* @param value
*/
public void push(E value){
myStack.add(value);
}

/**
* 出栈
* @return
*/
public E pop(){
if (isEmpty()){ // 先判断栈是否为空
throw new EmptyStackException();
}
// 删除栈顶端元素(即ArrayList中的最后一个元素)
return myStack.remove(myStack.size()-1);
}

/**
* 判断栈是否为空,为空则返回true
* @return
*/
private boolean isEmpty() {
return myStack.isEmpty();
}

/**
* 查看此栈顶部的对象
* @return
*/
public E peek(){
if (isEmpty()){ // 先判断栈是否为空
throw new EmptyStackException();
}
// 查看此栈顶部的对象(即ArrayList中的最后一个元素),但不将它从栈中删除
return myStack.get(myStack.size()-1);
}

/**
* 返回栈的大小
* @return
*/
public int size(){
return myStack.size();
}
}

Java Stack类

在JDK代码中,java.util.Stack类是继承了Vector类,来实现了栈的基本功能。

Stack常用方法如下:

  • boolean empty() :测试此堆栈是否为空
  • E peek() :查看此栈顶部的对象,而不将它从栈中删除
  • E pop() :在这个栈的顶部删除对象,并返回该对象的值作为该函数的值
  • E push(E item) :将一个项目推到这个堆栈的顶部
  • int search(Object o) :返回理想对象o距离栈顶的距离(距离栈顶最近的距离记为1),若对象o在栈中不存在,则返回-1(可以自己运行一下就很好理解了)

单调栈

在后面的刷题中遇到需要用单调栈来解决的题目,这里补充一下。

OI-WiKi单调栈

单调栈解题模板秒杀八道题

单调栈技巧总结

深入浅出搞通单调队列单调栈

顾名思义,单调栈即满足单调性的栈结构。与单调队列相比,其只在一端进行进出。

单调栈的重点分为单调

  • 单调指的是元素的的规律:递增(或递减)

  • 指的是元素只能从栈顶进行操作(出栈入栈都从栈顶进行操作)

单调栈和单调队列这两种数据结构理解起来挺清楚的,主要还是要结合题目来做,看看具体是怎么用代码来实现单调栈和单调队列的。


队列

什么是队列,队列及其应用(超详细)

队列,和栈一样,也是一种对数据的"存"和"取"有严格要求的线性存储结构。与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出。不仅如此,队列中数据的进出要遵循 “先进先出” 的原则,即最先进队列的数据元素,同样要最先出队列

Queue作为最基本的数据结构,在JDK代码中,也有它的实现,java.util.Queue类来实现了队列的基本功能。

队列的实现

队列存储结构的实现有以下两种方式:顺序队列(在顺序表的基础上实现的队列结构)和链队列(在链表的基础上实现的队列结构)

这里我们用链表实现队列,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import java.util.NoSuchElementException;

public class MyQueen<E> {
private Node front; // 队列头指针(注意,front节点是队列头的前一个节点,类似于头节点和首元节点,所以front节点始终为空节点)
private Node rear; // 队列尾指针(直接指向队列尾)
private int size; // 队列长度

/**
* 节点类
*/
class Node{
E data; /* 数据域 */
Node next; /* 指针域 */

public Node() {
}
public Node(E data) {
this.data = data;
}
public Node(E data, Node next) {
this.data = data;
this.next = next;
}
}

/**
* 构造方法
*/
public MyQueen() {
front = rear = new Node();
}

/**
* 向队尾插入元素
* @param data
* @return
*/
public boolean add(E data){
Node addNode = new Node(data);
rear.next = addNode;
rear = rear.next; /* 让rear始终指向队尾 */
size++;
return true;
}

/**
* 删除队列头的数据
* @return 被删除的数据
*/
public E remove(){
if (isEmpty()){
throw new NoSuchElementException("队列为空!");
}
/* 下面就是删除front节点的下一个节点(即队列头)的操作了,在之前的链表中已经多次实现了 */
Node removeNode = front.next; // front.next才是队列头
E removeData = removeNode.data;
front.next = removeNode.next;
removeNode = null; // 注意要清空被删除的节点
size--;
if (isEmpty()){ // 注意:如果元素出队后队列为空,要使front节点和rear在同一个位置(这样才能保证队列刚被初始化的时候)
front = rear;
}
return removeData;
}

/**
* 查看队列头的元素,但不删除
* @return 队列头数据
*/
public E peekFront(){
if (isEmpty()){
throw new NoSuchElementException("队列为空!");
}
return front.next.data; // 时刻注意:front.next才是队列头
}

/**
* 查看队列尾的元素,但不删除
* @return 队列尾数据
*/
public E peekRear(){
if (isEmpty()){
throw new NoSuchElementException("队列为空!");
}
return rear.data; // rear即为队列尾
}

/**
* 查询队列长度
* @return 队列长度
*/
public int size(){
return size;
}

/**
* 判断队列是否为空,为空则返回true
* @return
*/
private boolean isEmpty() {
return size == 0;
}

@Override
public String toString() {
if (isEmpty()) {
return "[]";
}
StringBuilder builder = new StringBuilder();
Node cur = front.next;
builder.append("[");
while (cur != null) {
builder.append(cur.data).append(", ");
cur = cur.next;
}
builder.replace(builder.length() - 2, builder.length(), "]");
return builder.toString();
}
}

Java Queue接口

在JDK代码中,也有它的实现,java.util.Queue类来实现了队列的基本功能。

Queue接口常用方法如下:

  • boolean add(E e) :向队列中插入元素,若队列已满,则返回false并抛出异常IllegalStateException
  • E remove() :删除此队列的头并返回被删除的值
  • E peek() :检索但不删除这个队列头,如果队列为空则返回null

JDK QUEUE队列

Queue接口的所有已知实现类有: AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedListLinkedList就可以作为队列使用), LinkedTransferQueue, PriorityBlockingQueue, PriorityQueue, SynchronousQueue 。

其中比较常用的非阻塞队列有:ArrayDeque(数组双端队列)PriorityQueue(优先队列)

比较常用的阻塞队列有:

  • ArrayBlockingQueue, (基于数组的并发阻塞队列) 底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择
  • LinkedBlockingQueue, (基于链表的FIFO阻塞队列) 底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列
  • PriorityBlockingQueue, (带优先级的无界阻塞队列) 是无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值

单调队列

在后面的刷题中遇到需要用单调队列来解决的题目,这里补充一下。

认识单调队列

深入浅出搞通单调队列单调栈

OI-Wiki 单调队列

顾名思义,单调队列的重点分为单调队列

  • 单调指的是元素的的规律:递增(或递减)

  • 队列指的是元素只能从队头和队尾进行操作

    (这里单调队列中的 “队列” 与正常的队列有一定的区别,稍后会提到)

关于单调队列概念的介绍就到这里,具体的可以去结合题目看看。比如说LeetCode 239. 滑动窗口最大值

在Java中,由于单调队列支持两端的删除操作,所以用Deque实现。Deque不考虑并发性问题,有两种实现方式(ArrayDequeLinkedList)。