阅读视图

发现新文章,点击刷新页面。
🔲 ☆

数据结构:栈与队列

本文将简单介绍栈和队列的概念,给出它们的 Java 实现以及原理解析。

栈(Stack)

栈是一种线性数据结构,这意味着每个数据节点(元素)都只有一个前驱和一个后继。栈的特点是后进先出(LIFO,Last In First Out),即最后入栈的元素最先出栈。

后进先出:想象一下,你面前有一叠盘子,你每次只能将新的盘子放在最上面,而取盘子时也只能从最上面取你最后放的盘子。

栈被操作的元素不以下标(index)为代表,而是以栈顶(top)为代表。栈顶是栈中最后一个元素,也是唯一一个可以被操作的元素。

栈的基本操作包括:

  • push:将元素压入栈顶
  • pop:将栈顶元素弹出,同时允许获取该元素的值
一个栈的 push 和 pop 操作示意图 [1]
一个栈的 push 和 pop 操作示意图 [1]

栈还可以有其他操作,如:

  • peek:查看栈顶元素
  • show:显示栈中所有元素
  • isEmpty:判断栈是否为空
  • isFull:判断栈是否已满
  • size:获取栈的大小(元素个数)

栈可以使用数组以及其他的数据结构(如 ArrayList、链表等)实现。下面是一个使用数组实现的栈的 Java 代码以及对应的原理解析。

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
// 本实现基于维基百科的实现 [1],并做了适当的简化
// 叫做 MyStack,而不是 Stack,是为了避免和 Java Util 中的 Stack 类重名造成混淆
class MyStack {
private int[] elements = new int[10]; // 栈的容量为 10,这个实现是一个静态(固定大小)的栈
private int top = 0; // 栈顶元素的下标,用于指示栈顶元素在数组中的位置

/**
* 将元素压入栈顶(入栈)
* @param element 要入栈的元素
*/
public void push(int element) {
if (!isFull()) {
// 仅在栈未满时才能入栈
// 将元素放入栈顶,然后栈顶下标加 1,这样下次入栈时就会放在新的栈顶位置
elements[top] = element;
top++;
// 也可以被写作
// elements[top++] = element;
} else {
System.out.println("The stack is full. Cannot push any more elements.");
}
}

/**
* 将栈顶元素弹出(出栈)
* @return 被弹出的元素(栈顶元素)
*/
public int pop() {
if (!isEmpty()) {
// 只有在栈非空时才能出栈
// 栈顶下标减 1,下标现在指向的是有元素的位置,然后返回该位置的元素
top--;
return elements[top];
// 也可以被写作
// return elements[--top];
} else {
// 栈为空时无法出栈
System.out.println("The stack is empty. Cannot pop any more elements.");
// 其实**绝对不应该**返回 -1,因为 -1 也是一个合法的元素,但为了简单起见,这里返回 -1
return -1;
}
}

/**
* 显示栈中所有元素
*/
public void show() {
if (isEmpty()) {
// 栈为空时无法显示任何元素
System.out.println("The stack is empty. Cannot show any elements.");
return;
}
for (int i = 0; i < top; i++) {
// 从栈底到栈顶依次显示元素
System.out.print(elements[i] + " ");
}
System.out.println();
}

/**
* 查看栈顶元素
*/
public void peek() {
if (isEmpty()) {
// 栈为空时无法查看栈顶元素
System.out.println("The stack is empty. Cannot peek any element.");
}
// 栈顶元素就是数组中下标为 top - 1 的元素
System.out.println("The top element is " + elements[top - 1]);
}

/**
* 判断栈是否为空
* @return 如果栈为空,返回 true;否则返回 false
*/
public boolean isEmpty() {
// 如果栈顶下标为 0,说明栈为空
return top == 0;
}

/**
* 判断栈是否已满
* @return 如果栈已满,返回 true;否则返回 false
*/
public boolean isFull() {
// 如果栈顶下标等于数组的长度,说明栈已满
return top == elements.length;
}

/**
* 获取栈的大小(元素个数)
* @return 栈的大小
*/
public int size() {
// top 的值就是栈的大小
return top;
}
}

public class Main {
public static void main(String[] args) {
// 可以自行测试 MyStack 类的功能,看看各项操作的表现
}
}

但是,在上述代码中,栈的容量是固定的,无法动态调整。但是我们通常希望栈的容量是动态的,即可以根据需要自动扩展或缩小。这时我们有两种方法:

  1. 使用数组实现栈,当栈满时,创建一个新的更大的数组,将原数组中的元素复制到新数组中,然后将新数组作为栈的存储结构。
  2. 使用 ArrayList 实现栈。ArrayList 是一个动态数组,可以根据需要自动扩展或缩小。

我们将会使用第二种方法(因为相对简单),下面是一个使用 ArrayList 实现的栈的 Java 代码。

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
package org.qianq;

import java.util.ArrayList;

class MyStackArrayList {
private ArrayList<Integer> elements = new ArrayList<>(); // 用于存放栈中元素的数组
private int top = 0; // 栈顶元素的下标,用于指示栈顶元素在数组中的位置

/**
* 将元素压入栈顶(入栈)
* @param element 要入栈的元素
*/
public void push(int element) {
// 实现与普通数组没有太大区别,只不过不再需要检查栈是否已满
// 将元素放入栈顶,然后栈顶下标加 1,虽然 ArrayList 会自动扩展,但这里还是加上这一步,这样访问栈顶元素时更方便
elements.add(element);
top++;
}

/**
* 将栈顶元素弹出(出栈)
* @return 被弹出的元素(栈顶元素)
*/
public int pop() {
if (!isEmpty()) {
// 只有在栈非空时才能出栈
// 栈顶下标减 1,下标现在指向的是有元素的位置,然后返回该位置的元素
top--;
return elements.remove(top);
} else {
// 栈为空时无法出栈
System.out.println("The stack is empty. Cannot pop any more elements.");
// 其实**绝对不应该**返回 -1,因为 -1 也是一个合法的元素,但为了简单起见,这里返回 -1
return -1;
}
}

/**
* 显示栈中所有元素
*/
public void show() {
if (isEmpty()) {
// 栈为空时无法显示任何元素
System.out.println("The stack is empty. Cannot show any elements.");
return;
}
for (int i = 0; i < top; i++) {
// 从栈底到栈顶依次显示元素
System.out.print(elements.get(i) + " ");
}
System.out.println();
}

/**
* 查看栈顶元素
*/
public void peek() {
if (isEmpty()) {
// 栈为空时无法查看栈顶元素
System.out.println("The stack is empty. Cannot peek any element.");
}
// 栈顶元素就是数组中下标为 top - 1 的元素
System.out.println("The top element is " + elements.get(top - 1));
}

/**
* 判断栈是否为空
* @return 如果栈为空,返回 true;否则返回 false
*/
public boolean isEmpty() {
// 如果栈顶下标为 0,说明栈为空
return top == 0;
}

/**
* 获取栈的大小(元素个数)
* @return 栈的大小(元素个数)
*/
public int size() {
// top 的值就是栈的大小
return top;
}
}

public class Main {
public static void main(String[] args) {
// 可以自行测试 MyStackArrayList 类的功能,看看各项操作的表现
}
}

你会发现其实两个版本的代码并没有太大区别。这些就是常用的栈的实现方法。

队列(Queue)

一个 Queue 在被操作时的示意图 [2]
一个 Queue 在被操作时的示意图 [2]

队列同样是一种线性数据结构,但它的特点是先进先出(FIFO,First In First Out),即最先入队的元素最先出队(就跟生活中排队一样)。

与栈不同,由于一个队列需要同时操作队首和队尾,因此队列有两个元素表示:队首(front)和队尾(rear)。队首是队列中第一个元素(最早入队的元素),队尾是队列中最后一个元素(最晚入队的元素)。

队列的基本操作包括:

  • enqueue:将元素入队
  • dequeue:将队首元素出队,同时允许获取该元素的值

队列还可以有其他操作,如:

  • show:显示队列中所有元素
  • isEmpty:判断队列是否为空
  • isFull:判断队列是否已满

我们仍然从一个使用数组实现的队列开始:

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
class MyQueue {
private final static int CAPACITY = 10; // 队列的最大容量
private int[] elements = new int[CAPACITY]; // 用于存放队列元素的数组
int front = 0, rear = -1, current_size = 0; // 队列的头指针、尾指针、当前元素个数(我们需要当前元素个数来判断队列是否为空或者满)

/**
* 将元素入队
* @param element 要入队的元素
*/
public void enqueue(int element) {
if (isFull()) {
// 如果队列已满,就不能再入队了
System.out.println("The queue is full. Cannot enqueue.");
return;
}
rear++; // 尾下标后移,这样就可以将元素放入正确的位置
elements[rear] = element; // 将元素放入目前队列的队尾
current_size++; // 当前元素个数加一
}

/**
* 将元素出队
* @return 被出队的元素
*/
public int dequeue() {
if (isEmpty()) {
// 如果队列为空,就不能再出队了
System.out.println("The queue is empty. Cannot dequeue.");
// 仍然为了简单起见,我们返回 -1 表示出队失败
return -1;
}
front++; // 头下标后移,这样就可以将元素出队
current_size--; // 当前元素个数减一
return elements[front - 1]; // 返回出队的元素
}

/**
* 显示队列中的元素
*/
public void show() {
for (int i = front; i <= rear; i++) {
System.out.print(elements[i] + " "); // 基本上就是从头走到尾,然后输出每个元素
}
System.out.println();
}

/**
* 判断队列是否为空
* @return 如果队列为空,返回 true;否则返回 false
*/
public boolean isEmpty() {
return current_size == 0;
}

/**
* 判断队列是否已满
* @return 如果队列已满,返回 true;否则返回 false
*/
public boolean isFull() {
return current_size == CAPACITY;
}
}

public class Main {
public static void main(String[] args) {
// 可以自行测试 MyQueue 类的功能,看看各项操作的表现
}
}

但是,同样的,这个队列的容量是固定的,无法动态调整。我们虽然一样可以使用 ArrayList 来调整队列的容量,但是这里我们可以实现一个环形队列(Circular Queue)。

环形队列可以被想象成一个首尾相连的环。当任何一个指针到达数组的末尾时,它会回到数组的开头。这样,我们就可以重复利用数组中已经被出队的元素的空间(否则出队的元素所占的空间就会被浪费掉)。

一个环形队列的示意图 [3]
一个环形队列的示意图 [3]
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
class MyCircularQueue {
private final static int CAPACITY = 10; // 队列的最大容量
private int[] elements = new int[CAPACITY]; // 用于存放队列元素的数组
int front = 0, rear = -1, current_size = 0; // 队列的头指针、尾指针、当前元素个数(我们需要当前元素个数来判断队列是否为空或者满)

/**
* 将元素入队
* @param element 要入队的元素
*/
public void enqueue(int element) {
if (isFull()) {
// 如果队列已满,就不能再入队了
// 此处我们仍然需要检查队列是否已满,因为我们的队列是循环队列,如果不检查,可能会导致未出队的元素被覆盖
System.out.println("The queue is full. Cannot enqueue.");
return;
}
rear = (rear + 1) % CAPACITY; // 尾下标后移,这样就可以将元素放入队尾
// 这里使用了取模运算,是为了实现循环队列,如果下标超出了数组的范围,到达了数组长度为值的下标,实际上就是回到了数组的开头(0)
// 然后如果继续往前走,实际上就是继续经过下标为 1, 2, 3, ... 的位置
elements[rear] = element; // 将元素放入目前队列的队尾
current_size++; // 当前元素个数加一
}

/**
* 将元素出队
* @return 被出队的元素
*/
public int dequeue() {
if (isEmpty()) {
// 如果队列为空,就不能再出队了
System.out.println("The queue is empty. Cannot dequeue.");
// 仍然为了简单起见,我们返回 -1 表示出队失败
return -1;
}
front = (front + 1) % CAPACITY; // 头下标后移,这样就可以将元素从队头取出
current_size--; // 当前元素个数减一
return elements[front - 1]; // 返回出队的元素
}

/**
* 显示队列中的元素
*/
public void show() {
for (int i = 0; i < current_size; i++) {
System.out.print(elements[(front + i) % CAPACITY] + " ");
// 这样可以保证从队头开始,依次输出队列中的元素,取模运算是为了实现循环队列中的元素输出
}
System.out.println();
}

/**
* 判断队列是否为空
* @return 如果队列为空,返回 true;否则返回 false
*/
public boolean isEmpty() {
return current_size == 0;
}

/**
* 判断队列是否已满
* @return 如果队列已满,返回 true;否则返回 false
*/
public boolean isFull() {
return current_size == CAPACITY;
}
}

public class Main {
public static void main(String[] args) {
// 可以自行测试 MyCircularQueue 类的功能,看看各项操作的表现
}
}

例题

Coming soon…

🔲 ☆

二维数组、类类型数组与 ArrayList

本文将简单介绍 Java 中的二维数组、以类为元素类型的数组以及 ArrayList。

二维数组

在深入讨论二维数组之前,我们先来复习一下一维数组。在 Java 中,一维数组由相同类型的元素组成,一个一维数组的常用语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个长度为 5 的 int 类型数组
int[] arr = new int[5];
// 获取数组的长度
int len = arr.length;
// 获取数组的第一个元素
int first = arr[0];
// 修改数组的第一个元素
arr[0] = 1;

// 数组的元素类型可以是原始数据类型 (primitive data type) ,也可以是一个类 (例如 String 类)
String[] strArray = new String[5];
// 或者任何一个用户自定义的类
MyClass[] myClassArray = new MyClass[5];

// 注意,与原始数据类型数组不同,以类为元素的数组在创建时并不会自动初始化(不会给元素授予默认值,每个元素都是 null),需要手动初始化
// 该示例也同样展示了一维数组的遍历
for (int i = 0; i < myClassArray.length; i++) {
myClassArray[i] = new MyClass();
}

现在发挥一下想象力,将一维数组想象成一排柜子,每个单元里都只能放同类的一样物品。但是,对一维数组来说,每排柜子都只有一个,二维数组则为这每排柜子增加了列的概念。

对 Java 来说,一个二维数组其实就是每个元素均为一个一维数组的数组。一个二维数组的常用语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个 3x3 的 int 类型二维数组
int[][] arr = new int[3][3];
// 获取数组中的第二个数组中的第一个元素
int num = arr[1][0];
// 修改数组中的第二个数组中的第一个元素
arr[1][0] = 1;
// 获取整体数组的长度
int len = arr.length;
// 获取每个元素数组的长度
int len1 = arr[0].length;

// 同理,二维数组的元素类型也可以是一个类
MyClass[][] myClassArray = new MyClass[3][3];
// 一个二维数组的简单遍历
for (int i = 0; i < myClassArray.length; i++) {
for (int j = 0; j < myClassArray[i].length; j++) {
myClassArray[i][j] = new MyClass();
}
}

例题

  1. 输入一个 4x4 的 int 类型二维数组,寻找其中的最大值并输出其坐标。

    解题方法
    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
    public class Main {
    public static void main(String[] args) {
    // 创建一个 4x4 的 int 类型二维数组
    int[][] arr = new int[][]{
    {182, 1723, 28, 12},
    {18, 238, 229, 128},
    {283, 2891, 38, 1},
    {0, -29, 382, 29}
    };

    // 创建一个变量用于存储最大值,其初始值为 int 类型的最小值,以确保数组中的任意值都会比其大
    int max = Integer.MIN_VALUE;
    // 创建两个变量用于存储最大值的坐标
    int x = 0, y = 0;

    // 遍历二维数组
    for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr[i].length; j++) {
    // 如果当前元素大于 max,则将 max 更新为当前元素,并记录其坐标
    if (arr[i][j] > max) {
    max = arr[i][j];
    x = i;
    y = j;
    }
    }
    }

    // 输出最大值及其坐标
    System.out.println("max: " + max + ", x: " + x + ", y: " + y);
    }
    }
  2. 输入一个 4x4 的 int 类型二维数组,使用冒泡排序 (bubble sort) 对其进行从大到小的排序。

    解题方法(较为简单)
    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
    public class Main {
    public static void main(String[] args) {
    // 创建一个 4x4 的 int 类型二维数组
    int[][] arr = new int[][]{
    {182, 1723, 28, 12},
    {18, 238, 229, 128},
    {283, 2891, 38, 1},
    {0, -29, 382, 29}
    };

    // 使用冒泡排序对二维数组进行排序
    // 冒泡排序的算法本质决定了最多只需要 n 次遍历即可完成排序,其中 n 为数组的长度
    for (int c = 0; c < arr.length * arr[0].length; c++) { // 由于二维数组的长度为 4x4,所以这里的 n 为 16,是大数组的长度乘以小数组的长度
    // 冒泡排序核心算法:将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置(我们需要从大到小排序)
    for (int i = 0; i < arr.length; i++) {
    int j;
    for (j = 0; j < arr[i].length - 1; j++) {
    // 将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置
    // 注意:此处 j 只到每行的倒数第二个元素,因为如果 j 到最后一个元素,那么 j + 1 就会越界
    if (arr[i][j] < arr[i][j + 1]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i][j + 1];
    arr[i][j + 1] = temp;
    }
    }
    // 但是,上面只检查到每行的倒数第二个元素肯定是不够的,我们还需要将每行的最后一个元素和下一行的第一个元素进行比较并按需交换
    if (i < 3) { // 此处的 if 确保我们不去检查最后一行的最后一个元素,它后面没有元素了
    if (arr[i][j] < arr[i + 1][0]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i + 1][0];
    arr[i + 1][0] = temp;
    }
    }
    }
    }
    }
    }
    解题方法(优化版)
    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
    public class Main {
    public static void main(String[] args) {
    // 创建一个 4x4 的 int 类型二维数组
    int[][] arr = new int[][]{
    {182, 1723, 28, 12},
    {18, 238, 229, 128},
    {283, 2891, 38, 1},
    {0, -29, 382, 29}
    };

    // 使用冒泡排序对二维数组进行排序
    // 冒泡排序的算法本质决定了最多只需要 n 次遍历即可完成排序,其中 n 为数组的长度
    bool swapped = true;
    while (swapped) { // 实际上,在某些情况下,我们不需要遍历 n 次就可以完成排序,所以我们可以通过判断上一次“排序”是否有交换来提前结束排序,如果上一次没有交换,那么说明数组已经排序完成
    swapped = false; // 每次遍历开始前,我们都假设没有交换
    // 其余算法与上面的解题方法相同
    // 冒泡排序核心算法:将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置(我们需要从大到小排序)
    for (int i = 0; i < arr.length; i++) {
    int j;
    for (j = 0; j < arr[i].length - 1; j++) {
    // 将每个元素与其后一个元素进行比较,如果前者小于后者,则交换两者的位置
    // 注意:此处 j 只到每行的倒数第二个元素,因为如果 j 到最后一个元素,那么 j + 1 就会越界
    if (arr[i][j] < arr[i][j + 1]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i][j + 1];
    arr[i][j + 1] = temp;
    swapped = true;
    }
    }
    // 但是,上面只检查到每行的倒数第二个元素肯定是不够的,我们还需要将每行的最后一个元素和下一行的第一个元素进行比较并按需交换
    if (i < 3) { // 此处的 if 确保我们不去检查最后一行的最后一个元素,它后面没有元素了
    if (arr[i][j] < arr[i + 1][0]) {
    int temp = arr[i][j];
    arr[i][j] = arr[i + 1][0];
    arr[i + 1][0] = temp;
    swapped = true;
    }
    }
    }
    }
    }
    }

类数组

正如上面提到过的,数组的元素可以不是 int, double, char 等原始数据类型,也可以是一个类。我们可以使用这个特性来完成一些比较复杂的操作。

例题

创建一个类 Student,包含两个属性 namescore,分别为学生的姓名和分数。然后,输入一个 Student 类型的数组,对其按照分数从大到小进行排序。最后以此输出学生的姓名和分数。

解题方法
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
class Student {
// 学生类,包含两个属性 name 和 score
private String name;
private int score;

// 构造函数
public Student(String name, int score) {
this.name = name;
this.score = score;
}

// 获取学生的成绩
public int getScore() {
return score;
}
}

public class Main {
public static void main(String[] args) {
// 创建一个长度为 3 的 Student 类型数组
Student[] students = new Student[3];

for (int i = 0; i < students.length; i++) {
// 初始化学生数组的每个元素
// 此处我使用了 Student 加元素的索引来命名学生的姓名,以及一个随机数来模拟学生的分数
// 实际上,这里的初始化过程可以是任何你想要的,可以是用户输入
students[i] = new Student("Student" + i, (int)(Math.random() * 100));
}

// 使用冒泡排序对学生数组按成绩进行排序
boolean swapped = true;
while (swapped) {
swapped = false;
for (int i = 0; i < students.length - 1; i++) {
if (students[i].getScore() < students[i + 1].getScore()) {
Student temp = students[i];
students[i] = students[i + 1];
students[i + 1] = temp;
swapped = true;
}
}
}

// 输出学生的姓名和分数
for (Student student : students) {
System.out.println(student.name + ": " + student.getScore());
}
}
}

总而言之,使用类作为数组的元素类型可以让我们在处理一些复杂的数据时更加方便。以上述例题为例,如果我们分开使用两个数组来存储学生的姓名和分数,由于我们只在排序成绩,但同时需要保证姓名和分数的对应关系,相关操作会变的相当麻烦。但是,如果我们使用一个 Student 类型的数组,那么我们就可以保证姓名和分数的对应关系,从而更加方便地进行排序。

ArrayList

ArrayList 是 Java 中的一个类,它仍然是一个 Array (数组)。但是,与数组不同的是,ArrayList 的长度是可以动态变化的。在 Java 中,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
import java.util.ArrayList; // 导入 ArrayList 类,你的 IDE 或许可以帮你把这件事做了。

public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>(); // 创建一个 String 类型的 ArrayList
strings.add("Hello"); // 使用 add 方法向 ArrayList 中添加元素,你可以直接写入元素的值,该元素会被添加到 ArrayList 的末尾
strings.add("Java");
strings.add("Programming");
strings.add("Language");
strings.add(1, "World"); // 你也可以使用 add 方法的重载版本,指定元素的索引,该元素会被添加到指定索引的位置
System.out.println(strings); // [Hello, World, Java, Programming, Language]
strings.remove(2); // 使用 remove 方法移除指定索引的元素
System.out.println(strings); // [Hello, World, Programming, Language]
strings.remove("World"); // 使用 remove 方法移除指定元素
System.out.println(strings); // [Hello, Programming, Language]

ArrayList<Integer> integers = new ArrayList<>(); // 创建一个 Integer 类型的 ArrayList
// 值得注意的是,ArrayList 的元素类型不可以是原始数据类型,所以我们需要使用包装类 (wrapper class) 来代替
// 你可以将包装类理解为只有一个对应的原始数据类型的类,例如 Integer 对应 int
integers.add(1); // 你仍然可以使用 add 方法向 ArrayList 中添加元素
integers.add(2);
integers.add(3);
integers.add(4);
integers.add(5);
integers.add(1, 6); // 你也可以使用 add 方法的重载版本,指定元素的索引,该元素会被添加到指定索引的位置
System.out.println(integers); // [1, 6, 2, 3, 4, 5]
integers.remove(2); // 使用 remove 方法移除指定索引的元素
System.out.println(integers); // [1, 6, 3, 4, 5]
integers.remove(Integer.valueOf(6)); // 使用 remove 方法移除指定元素,此处值得注意的是,你不能直接写入 “6”,因为这样会被认为是索引,而不是元素,所以你需要使用包装类的 valueOf 方法
System.out.println(integers); // [1, 3, 4, 5]
// 你可以使用 size 方法获取 ArrayList 的长度
System.out.println(integers.size()); // 4
// 虽然 ArrayList 的长度是可以动态变化的,但是如果频繁更改 ArrayList 的长度,会对性能产生一定的影响,所以你可以使用 ensureCapacity 方法来提前设置 ArrayList 的容量
// 你也可以在初始化 ArrayList 时指定其容量而不再需要单独使用 ensureCapacity 方法,例如:ArrayList<Integer> integers = new ArrayList<>(10);
integers.ensureCapacity(10);
// 但是 ensureCapacity 方法只是设置了 ArrayList 的容量,size 方法返回的长度仍然是实际元素的个数
System.out.println(integers.size()); // 4
integers.add(6);
integers.add(7);
// 当你确认 ArrayList 的长度不会再发生变化时,你可以使用 trimToSize 方法来释放多余的空间,此时 ArrayList 的容量会被设置为实际元素的个数
integers.trimToSize();
// 同样的,size 方法返回的长度仍然是实际元素的个数
System.out.println(integers.size()); // 6

integers.set(0, 10); // 你可以使用 set 方法来修改指定索引的元素,此处将索引为 0 的元素修改为 10
System.out.println(integers); // [10, 3, 4, 5, 6, 7]
// 你也可以使用 get 方法来获取指定索引的元素
System.out.println(integers.get(0)); // 10
}
}

将 ArrayList 转换为数组

有时候,我们需要将 ArrayList 转换为数组。我们有两种方法可以做到这一点:

第一种方法是创建一个相同长度的,新的数组,然后遍历 ArrayList,将其中的元素逐个添加到新数组中。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.*;

public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
strings.add("String1");
strings.add("String2");
strings.add("String3");
strings.add("String4");

// 创建一个相同长度的新数组
String[] array = new String[strings.size()];

// 遍历 ArrayList,将其中的元素逐个添加到新数组中
for (int i = 0; i < strings.size(); i++) {
array[i] = strings.get(i);
}
}
}

或者,我们可以使用 toArray 方法来完成这个任务。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.*;

public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
strings.add("String1");
strings.add("String2");
strings.add("String3");
strings.add("String4");

// 使用 toArray 方法将 ArrayList 转换为数组
// 注意,传入给 toArray 方法的参数是一个新的数组,该数组的长度为 ArrayList 的长度
String[] array = strings.toArray(new String[strings.size()]);
}
}

例题

  1. 写一个程序,创建一个长度为 10 的 String 类型的 ArrayList,然后向其中添加 10 个字符串。接着创建一个长度为 10 的 Integer 类型的 ArrayList,然后向其中添加 10 个整数。最后,将 String 类型的 ArrayList 中元素索引为偶数的元素删除掉,将 Integer 类型的 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
    import java.util.*;

    public class Main {
    public static void main(String[] args) {
    ArrayList<String> strings = new ArrayList<>();
    strings.add("String0");
    strings.add("String1");
    strings.add("String2");
    strings.add("String3");
    strings.add("String4");
    strings.add("String5");
    strings.add("String6");
    strings.add("String7");
    strings.add("String8");
    strings.add("String9");

    ArrayList<Integer> integers = new ArrayList<>();
    integers.add(0);
    integers.add(1);
    integers.add(2);
    integers.add(3);
    integers.add(4);
    integers.add(5);
    integers.add(6);
    integers.add(7);
    integers.add(8);
    integers.add(9);

    // 创建新的 ArrayList 并不难,但是删除元素的时候需要注意,因为每次删除元素后,ArrayList 的长度会发生变化,需要删除的元素的索引也会发生变化,所以我们需要点小技巧
    // 接下来两个循环都使用了一些神奇的算法,如果你不太理解,可以尝试在草稿纸上画出数组,然后一步一步地模拟删除的过程,你就会发现其中的规律
    // 规律:为了删除索引为偶数的元素,我们其实正好只需要删除从 0 开始,每次 +1 的位置的元素,直至数组的长度的一半;对于奇数的情况,我们其实只需要删除从 1 开始,其他同理

    // 删除 String 类型的 ArrayList 中索引为偶数的元素
    int stringsSize = strings.size();
    for (int i = 0; i < stringsSize / 2; i++) {
    strings.remove(i);
    }

    // 删除 Integer 类型的 ArrayList 中索引为奇数的元素
    int integersSize = integers.size();
    for (int i = 1; i < integersSize / 2 + 1; i++) {
    integers.remove(i);
    }
    }
    }
  2. 写一个程序,创建一个长度为 10 的 String 类型的 ArrayList,然后向其中添加 10 个字符串。最后删除掉所有重复的字符串。

    解题方法
    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
    import java.util.*;

    public class Main {
    public static void main(String[] args) {
    ArrayList<String> strings = new ArrayList<>();
    strings.add("String");
    strings.add("String1");
    strings.add("String");
    strings.add("String3");
    strings.add("String");
    strings.add("String5");
    strings.add("String");
    strings.add("String7");
    strings.add("String");
    strings.add("String9");

    // 实际上,我们只需要将每个元素和它后面的所有元素进行一下比较就可以了
    // 如果后面的元素和当前元素相同,那么我们就可以删除后面的元素
    for (int i = 0; i < strings.size(); i++) {
    for (int j = i + 1; j < strings.size(); j++) {
    if (strings.get(i).equals(strings.get(j))) {
    strings.remove(j);
    j--; // 此处要注意,删除元素后,后面的元素会向前移动,所以我们需要将 j 减一,以确保被向前移动的元素也会被检查
    }
    }
    }
    }
    }
❌