加载中...

目录


线性表

概念

线性表(Linear List)是由n(n≥0)个数据元素结点a[0],a[1],a[2]…,a[n-1]组成的有限序列。

其中:

  • 数据元素的个数n定义为线性表的长度 = "list".length()
  • "list".length() = 0(表里没有一个元素)时称为空表。
  • 将非空的线性表(n>=1)记作:(a[0],a[1],a[2],…,a[n-1]
  • 数据元素a[i]0≤i≤n-1)只是个抽象符号,其具体含义在不同情况下可以不同。

一个数据元素可以由若干个数据项组成。数据元素称为记录,含有大量记录的线性表又称为文件。

线性表具有以下特点:

  • 存在一个唯一的没有前驱的(头)数据元素;
  • 存在一个唯一的没有后继的(尾)数据元素;
  • 此外,每一个数据元素均有一个直接前驱和一个直接后继数据元素。

常见的线性表

我们总结,线性表是一种按照线性的顺序存储元素的数据结构,其中每个元素都有一个前驱和一个后继,除了第一个元素没有前驱,最后一个元素没有后继。

常见的线性表包括以下几种:

  1. 数组(Array): 数组是一种基本的线性表,它在内存中以连续的方式存储元素。数组具有固定的大小,通过索引可以直接访问元素。
  2. 链表(Linked List): 链表是由节点组成的数据结构,每个节点包含数据和一个指向下一个节点的引用。链表可以分为单链表、双向链表和循环链表等。
  3. 栈(Stack): 栈是一种具有后进先出(LIFO)特性的线性表。元素的插入和删除只能发生在栈的顶部。
  4. 队列(Queue): 队列是一种具有先进先出(FIFO)特性的线性表。元素的插入(入队)发生在队列的尾部,而删除(出队)发生在队列的头部。

数组

数组(Array),由相同数据类型的元素(element)的集合组成的数据结构,分配一块连续的内存来存储。

初始化

初始化一个数组(不给定初始值):

1
2
3
4
5
6
7
8
9
10
// 创建一个名为arr的整数数组
int[] arr = new int[5];

// 上述语句分为两部分:
// 1. int[]:声明一个整数数组
// 2. new int[5]:使用new关键字创建一个包含5个整数元素的新数组

// 注意:数组索引从0开始,因此这个数组的有效索引范围是0到4(总共5个元素)

// 现在,arr是一个长度为5的整数数组,所有元素的初始值为0(基本数据类型的默认值)

初始化一个数组(给定初始值):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建一个名为nums的整数数组,并初始化数组元素
int[] nums = { 1, 3, 2, 5, 4 };

// 上述语句分为两部分:
// 1. int[]:声明一个整数数组
// 2. { 1, 3, 2, 5, 4 }:初始化数组元素,数组长度为5

// 注意:这种初始化方式在声明数组的同时为其赋初值

// 数组元素的含义:
// nums[0] = 1:数组的第一个元素(索引为0)的值为1
// nums[1] = 3:数组的第二个元素(索引为1)的值为3
// nums[2] = 2:数组的第三个元素(索引为2)的值为2
// nums[3] = 5:数组的第四个元素(索引为3)的值为5
// nums[4] = 4:数组的第五个元素(索引为4)的值为4

// 注意:数组索引从0开始,因此这个数组的有效索引范围是0到4(总共5个元素)

访问元素

在数组中访问元素非常高效,我们可以在 𝑂(1) 时间内随机访问数组中的任意一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义一个方法randomAccess,该方法接收一个整数数组nums作为参数
int randomAccess(int[] nums) {

// 生成一个随机索引,范围在数组长度内
int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);

// 获取数组中随机索引对应的元素值
int randomNum = nums[randomIndex];

// 返回随机获取的数组元素值
return randomNum;
}

// 方法说明:
// 1. randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length):
// 通过ThreadLocalRandom生成一个位于 [0, nums.length) 范围内的随机整数,用作数组的索引。

// 2. int randomNum = nums[randomIndex]:
// 通过随机生成的索引获取数组中对应索引位置的元素值,即随机访问数组中的一个元素。

// 3. return randomNum:
// 将随机获取的数组元素值作为方法的返回值。

插入元素

数组是一种存储固定大小元素的数据结构。数组的元素在内存中是连续存储的,也就是说,它们是"紧挨着的"。当你想在数组中间插入一个元素时,必须将该元素之后的所有元素都向后移动一位,再将待插入的元素插入到中间位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在数组 nums 的索引 index 处插入元素 num
void insert(int[] nums, int num, int index) {

// 使用循环从数组末尾开始,将元素逐个向后移动,为新元素腾出插入位置
for (int i = nums.length - 1; i > index; i--) {
nums[i] = nums[i - 1];
}

// 在指定索引位置插入新元素
nums[index] = num;
}

// 方法说明:
// 1. for (int i = nums.length - 1; i > index; i--):
// 从数组末尾开始循环,将索引大于给定index的元素逐个向后移动,为新元素腾出插入位置。

// 2. nums[i] = nums[i - 1]:
// 将当前索引位置的元素的值设置为前一个索引位置的元素的值,实现向后移动。

// 3. nums[index] = num:
// 在指定索引位置插入新元素,将新元素的值赋给数组对应索引位置。

删除元素

与插入元素类似,如果你想删除数组中某一位置的元素,需要将后面的元素都向前移动一位。

删除元素后,原先末尾的元素就已经“无意义”了,因此不用特意修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 删除数组 nums 中索引为 index 处的元素
void remove(int[] nums, int index) {

// 使用循环从指定索引位置开始,将元素逐个向前移动,实现删除指定位置的元素
for (int i = index; i < nums.length - 1; i++) {
nums[i] = nums[i + 1];
}
}

// 方法说明:
// 1. for (int i = index; i < nums.length - 1; i++):
// 从指定索引位置开始循环,将索引大于等于给定index的元素逐个向前移动,实现删除指定位置的元素。

// 2. nums[i] = nums[i + 1]:
// 将当前索引位置的元素的值设置为后一个索引位置的元素的值,实现向前移动。

遍历数组

在Java中,遍历数组即可以通过索引来遍历,也可以通过for-each方式来遍历。

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
// 遍历数组
void traverse(int[] nums) {

// 初始化一个计数器变量count,用于累加数组元素的值
int count = 0;

// 使用传统的for循环遍历数组
for (int i = 0; i < nums.length; i++) {
// 将当前索引位置的元素值累加到计数器
count += nums[i];
}

// 使用增强型for循环(for-each)遍历数组
for (int num : nums) {
// 将当前元素值累加到计数器
count += num;
}
}

// 方法说明:
// 1. int count = 0;:
// 初始化一个计数器变量count,用于累加数组元素的值。

// 2. for (int i = 0; i < nums.length; i++):
// 使用传统的for循环遍历数组,将每个索引位置的元素值累加到计数器。

// 3. for (int num : nums):
// 使用增强型for循环(for-each)遍历数组,将每个元素值累加到计数器。

查找元素

在数组中查找特定的元素,可以通过一层循环,遍历数组元素,在每轮循环中判断当前数组元素是否为待查找的元素。若匹配,则输出对应索引,否则返回-1,代表查无此元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在数组 nums 中查找指定元素 target
int find(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) {
return i;
}
}
return -1;
}

// 方法说明:
// 1. for (int i = 0; i < nums.length; i++):
// 使用传统的for循环遍历数组,检查每个索引位置的元素是否等于目标整数。

// 2. if (nums[i] == target):
// 判断当前索引位置的元素是否等于目标整数。

// 3. return i;:
// 如果找到目标整数,返回当前索引位置。

// 4. return -1;:
// 如果未找到目标整数,返回-1表示未找到。

数组扩容

在Java中,数组的长度是固定不可变的,一旦数组被创建后,其长度就不能更改。如果你需要扩容数组,通常的做法是创建一个新数组,并将原数组中的元素复制到新数组中。

举个例子,比如这里给定了一个旧数组 oldArray,然后创建了一个新数组 newArray(长度是旧数组长度的两倍),最后,将旧数组的元素复制到新数组中。

1
2
3
4
5
6
7
8
9
10
11
// 定义旧数组 oldArray,包含元素 1, 2, 3, 4, 5
int[] oldArray = {1, 2, 3, 4, 5};

// 创建新数组 newArray,长度是旧数组的两倍
int[] newArray = new int[oldArray.length * 2];

// 使用循环将旧数组的元素复制到新数组中
for (int i = 0; i < oldArray.length; i++) {
// 将旧数组中的元素逐个复制到新数组的相同索引位置
newArray[i] = oldArray[i];
}

当前,你也可以通过System.arraycopy 方法将一个数组的内容复制到另一个数组(适用于大规模数据的复制)

1
2
3
4
5
6
7
8
int[] oldArray = {1, 2, 3, 4, 5};
int[] newArray = new int[oldArray.length * 2];
System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
// oldArray:源数组,要从这个数组复制元素
// 0:源数组起始位置,从源数组的第0个元素开始复制
// newArray:目标数组,要将元素复制到这个数组
// 0:目标数组起始位置,从目标数组的第0个位置开始粘贴
// oldArray.length:要复制的元素个数,即源数组的长度

或者,也可以使用 Arrays.copyOf 方法来复制数组元素:

1
2
3
4
5
int[] oldArray = {1, 2, 3, 4, 5};
int newLength = oldArray.length * 2;

// Arrays.copyOf(原数组, 新数组的长度);
int[] newArray = Arrays.copyOf(oldArray, newLength);

数组的优点和局限性

数组是一种在编程中常用的数据结构,具有一些优点和一些局限性。

优点:

  1. 快速访问: 数组中的元素通过索引直接访问,这使得对元素的访问速度非常快,支持随机访问,时间复杂度为 O(1)
  2. 内存连续: 数组中的元素在内存中是连续存储的,这有助于缓存性能的提升,空间效率高。
  3. 简单易用: 数组的使用非常简单,适合存储和访问一组相同类型的元素。

局限性:

  1. 固定大小: 数组在创建时需要指定固定的大小,这导致了无法动态调整数组的长度。如果需要动态大小的数据结构,可能需要使用其他集合类。
  2. 插入和删除开销大: 在数组中插入或删除元素通常需要移动其他元素,这导致了较大的时间复杂度(O(n))。
  3. 浪费空间: 如果数组的大小超过实际需要的大小,会导致内存浪费。反之,如果数组太小,可能需要频繁地进行扩容操作。
  4. 不适合频繁的查找和删除: 数组适用于快速访问,但对于频繁的查找和删除操作,可能不是最佳选择。

动态数组

在Java中,ArrayList 是一种动态数组,属于 java.util 包。

ArrayList与内置数组的区别在于,数组的大小无法修改(如果要向数组添加或删除元素,必须创建一个新数组)。而ArrayList可以随时添加和删除元素。当 ArrayList 的元素数量超过其当前容量时,ArrayList 会自动增加其容量。

也就是ArrayList帮你实现好了扩容,底层通过创建一个新的数组,将原数组的元素复制到新数组中来实现的。默认情况下,扩容时会将容量翻1.5倍。

ArrayList 是使用泛型实现的,可以存储任意类型的对象。在创建 ArrayList 时,可以指定元素的类型。

下面是 ArrayList 的常见用法:

  1. 创建ArrayList对象:
1
2
3
import java.util.ArrayList;

ArrayList<String> cars = new ArrayList<String>();
  1. 添加元素:
1
2
3
4
cars.add("Volvo");
cars.add("BMW");
cars.add("Ford");
cars.add("Mazda");
  1. 访问元素:
1
String car = cars.get(0);
  1. 修改元素:
1
cars.set(0, "Opel");
  1. 删除元素:
1
cars.remove(0);
  1. 清空ArrayList
1
cars.clear();
  1. 获取ArrayList大小:
1
int size = cars.size();
  1. 遍历ArrayList

使用for循环:

1
2
3
for (int i = 0; i < cars.size(); i++) {
System.out.println(cars.get(i));
}

或使用for-each循环:

1
2
3
for (String car : cars) {
System.out.println(car);
}
  1. 排序ArrayList

使用Collections.sort()方法:

1
Collections.sort(cars);  // 对字符串进行排序

链表

概述

链表(Linked list)是一种线性表。由一系列节点对象组成,每个节点包含数据元素和一个指向下一个节点的引用,也就是说,各个节点通过“引用”相连接。

1
2
3
4
5
6
7
8
9
// 链表节点类:
class ListNode {
int elem; // 节点的值
ListNode next; // 指向下一个节点的引用(也称为指针域)

ListNode(int element) { // 构造器
elem = element;
}
}

“引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。因此,链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。”

链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理,同时,也失去了快速随机存取的优点(数组),同时增大了内存开销(存储下一个节点)。

链表类型

常见链表类型有三种,分别是单链表,环形链表和双向链表。对比如下:

特性 单向链表 环形链表 双向链表
简介 简称单链表,是一种常规链表,其节点包含值和指向下一节点的引用两项数据,头节点指向第一个实际存储数据的节点,尾节点指向null 首尾相接,将单链表的尾节点指向头节点就得到了一个环形链表。在环形链表中,任意节点都可以视为头节点。 与单链表相比,双向链表记录了两个方向的引用,也就是前驱结点prev和后继节点next,因此更加灵活,可以朝两个方向遍历链表。
节点结构 数据 + 下一节点的指针 数据 + 下一节点的指针 数据 + 下一节点的指针 + 前一节点的指针
遍历特点 单向遍历 循环遍历 双向遍历(前向和后向)

头节点

在使用单链表的时候,我们通常会使用一个头节点(head)。

该头节点不存储数据,而是指向第一个实际存储数据的节点

尾节点就是最后一个节点,该节点指向的下一个节点为null

为什么需要头节点?

使用头节点,可以简化对链表的操作,提供一个固定的起点。如果没有头节点,我们在对链表进行插入、删除等操作时需要考虑处理第一个节点的特殊情况。头节点的存在使得链表的操作更加一致,使代码更加简洁和统一。

单链表的使用效果

下面,我们来编写一个单链表 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
public class Main {

public static void main(String[] args) {

// 创建一个链表实例
LinkedList linkedList = new LinkedList();

// 向链表的尾部添加6个元素
linkedList.addLast(10);
linkedList.addLast(20);
linkedList.addLast(30);
linkedList.addLast(40);
linkedList.addLast(50);
linkedList.addLast(60);

// 打印链表
System.out.println("(1) 初始化后,当前链表为:" + linkedList.printList());

// 输出链表的大小
System.out.println("(2) 链表的大小:" + linkedList.size());

// 在指定位置插入元素
linkedList.add(1, 15);
System.out.println("(3) 在索引为1的位置插入元素15后,链表:" + linkedList.printList());

//在指定位置插入元素
linkedList.addFirst(100);
System.out.println("(4) 在链表的头部添加一个元素100,链表:" + linkedList.printList());

// 判断链表是否含有指定元素
System.out.println("(5) 判断链表是否包含元素15:" + linkedList.contains(15));

// 设置链表中指定位置的元素值
linkedList.set(5, 15);
System.out.println("(6) 将链表中索引为5的元素修改值为15,链表:" + linkedList.printList());

// 删除链表中指定索引处的元素
linkedList.remove(1);
System.out.println("(7) 将链表中索引为1的元素移除,链表:" + linkedList.printList());

// 删除链表中第一个出现的指定元素
linkedList.removeElement(50);
System.out.println("(8) 删除值为50的节点后,链表:" + linkedList.printList());

// 返回链表中指定位置的元素
System.out.println("(9) 链表中索引为3的节点的值为:" + linkedList.get(3));

// 查找链表中指定元素从前往后第一次出现的索引
System.out.println("(10) 链表中第一次出现值为15的节点,其索引为:" + linkedList.indexOf(15));

// 查找链表中指定元素最后一次出现的索引
System.out.println("(11) 链表中最后一次出现值为15的节点,其索引为:" + linkedList.lastIndexOf(15));
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
(1) 初始化后,当前链表为:[10 → 20 → 30 → 40 → 50 → 60]
(2) 链表的大小:6
(3) 在索引为1的位置插入元素15后,链表:[10 → 15 → 20 → 30 → 40 → 50 → 60]
(4) 在链表的头部添加一个元素100,链表:[100 → 10 → 15 → 20 → 30 → 40 → 50 → 60]
(5) 判断链表是否包含元素15:true
(6) 将链表中索引为5的元素修改值为15,链表:[100 → 10 → 15 → 20 → 30 → 15 → 50 → 60]
(7) 将链表中索引为1的元素移除,链表:[100 → 15 → 20 → 30 → 15 → 50 → 60]
(8) 删除值为50的节点后,链表:[100 → 15 → 20 → 30 → 15 → 60]
(9) 链表中索引为3的节点的值为:30
(10) 链表中第一次出现值为15的节点,其索引为:1
(11) 链表中最后一次出现值为15的节点,其索引为:4

实现单链表

Node

在链表中,其组成元素都是节点(Node)对象。并且每个节点都包含两项数据:

  1. 节点的“值”(elem
  2. 指向下一个节点的“引用”(next

我们来写一下这个Node类:

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
// 定义一个名为Node的类,表示链表中的节点
public class Node {

// 私有属性,表示节点中的元素值
private int elem;

// 私有属性,表示当前节点的下一个节点(也称为指针域)
private Node next;

// 构造方法,接收一个整数元素值和下一个节点作为参数
public Node(int element, Node next) {
this.elem = element;
this.next = next;
}

// 默认构造方法,创建一个元素值为0,下一个节点为null的节点
public Node() {
this(0, null);
}

// 设置元素值的方法
public void setElem(int element) {
this.elem = element;
}

// 获取元素值的方法
public int getElem() {
return this.elem;
}

// 设置下一个节点的方法
public void setNext(Node next) {
this.next = next;
}

// 获取下一个节点的方法
public Node getNext() {
return this.next;
}
}

// 类说明:
// 1. Node类表示链表中的节点,每个节点包含一个元素值和对下一个节点的引用。

// 2. 构造方法 Node(int element, Node next):
// 用于创建具有给定元素值和下一个节点引用的节点。

// 3. 默认构造方法 Node():
// 创建一个元素值为0,下一个节点为null的节点。

// 4. setElem(int element):
// 设置节点的元素值。

// 5. getElem():
// 获取节点的元素值。

// 6. setNext(Node next):
// 设置节点的下一个节点引用。

// 7. getNext():
// 获取节点的下一个节点引用。

LinkedList

这里我们定义一个类,用于表示一个链表类。

1
2
3
4
5
6
7
8
public class LinkedList {

private Node head; // 头节点

public LinkedList() {
this.head = new Node(); // 创建头节点,数据域和指针域均为空
}
}

这样,我们就成功地初始化了一个单链表,可以在这个基础上进行后续的节点插入、删除等操作。

size

size() 方法用于获取链表的大小,即链表中包含的元素数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 获取链表的大小,即链表中包含的元素数量。
* @return 链表的大小
*/
public int size() {
// 初始化计数器为0,用于记录链表的大小
int count = 0;

// 从头节点开始,遍历链表,直到遍历到末尾节点
Node current = head.getNext();
while (current != null) {
count++;
current = current.getNext();
}

// 返回链表的大小,即元素的数量
return count;
}

isEmpty

判断链表是否为空,可以有两种策略:

  • 策略一:使用size()方法,判断链表存储的元素数量是否为0。
  • 策略二:判断头节点的指针域是否为null
1
2
3
4
5
6
7
8
/**
* 检查链表是否为空。
* @return 如果链表为空,则返回true;否则返回false
*/
public boolean isEmpty() {
// 判断链表是否为空,通过检查头节点的下一个节点是否为null来判断
return head.getNext() == null;
}

clear

clear()方法用于清空链表,移除所有节点。实际就是将头节点的下一个节点设为null

1
2
3
4
5
6
7
/**
* 清空链表,移除所有节点。
*/
public void clear() {
// 将头节点的下一个节点设为null,即移除所有节点
head.setNext(null);
}

contains

contains(int element)方法用于判断链表是否含有指定元素element

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 判断链表是否含有指定元素。
*
* @param element 要判断的元素。
* @return true 如果链表包含指定元素,false 如果链表不包含指定元素。
*/
public boolean contains(int element) {
// 从头节点开始遍历链表,查找是否包含指定元素
Node current = head.getNext();
while (current != null) {
if (current.getElem() == element) {
return true; // 找到指定元素,返回true
}
current = current.getNext();
}
return false; // 链表中不包含指定元素
}

set

set(int index, int element)方法用于设置链表中指定位置的元素值,并且返回原元素的值。

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
/**
* 设置链表中指定位置的元素值。
*
* @param index 要设置元素的位置,从0开始计数。
* @param element 要设置的元素值。
* @return 被替换的原元素的值。
* @throws IndexOutOfBoundsException 如果指定位置超出链表范围。
*/
public int set(int index, int element) throws IndexOutOfBoundsException {
// 检查指定位置是否合法
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("指定位置超出链表范围");
}

// 找到指定位置的节点
Node current = head.getNext();
for (int i = 0; i < index; i++) {
current = current.getNext();
}

// 获取原元素的值
int oldValue = current.getElem();

// 设置新的元素值
current.setElem(element);

return oldValue;
}

printList

printList方法用于打印链表。

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
/**
* 打印链表,每个节点之间用 "→" 进行连接。
*
* @return 表示链表的字符串。
*/
public String printList() {
StringBuilder result = new StringBuilder();
result.append("[");

// 遍历链表,将每个节点的元素拼接到字符串中
Node current = head.getNext();
while (current != null) {
result.append(current.getElem());

// 判断是否为最后一个节点,如果不是,则添加 "→"
if (current.getNext() != null) {
result.append(" → ");
}

current = current.getNext();
}
result.append("]");

return result.toString();
}

插入元素

add

add(index, element)方法用于向在指定位置插入新元素。

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
/**
* 向链表的指定位置插入元素。
*
* @param index 插入的位置,从0开始计数。
* @param element 要插入的元素。
* @throws IndexOutOfBoundsException 如果插入位置超出链表范围。
*/
public void add(int index, int element) throws IndexOutOfBoundsException {
// 创建新节点
Node newNode = new Node(element, null);

// 检查插入位置是否合法
if (index < 0 || index > size()) {
throw new IndexOutOfBoundsException("插入位置超出链表范围");
}

// 找到插入位置的前一个节点
Node current = head;
for (int i = 0; i < index; i++) {
current = current.getNext();
}

// 插入新节点
newNode.setNext(current.getNext());
current.setNext(newNode);
}

Node current = head; 用于初始化一个临时节点(current),并将其设置为链表头节点(head),从头节点开始遍历链表。循环的次数为插入位置(index),每次迭代将current移动到下一个节点,最终将current移动到插入位置的前一个节点。

最后两行代码,用于将新节点的下一个节点设置为当前节点的下一个节点,然后将当前节点的下一个节点设置为新节点。这样就成功地在指定位置插入了新节点。

addLast

addLast(int element)方法,用于向链表的尾部添加一个新的元素,插入的新元素总是位于链表的末尾(也就是指针域为null的节点),我们称为尾插法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 在链表的末尾添加一个新节点,该节点包含指定的元素值。
* @param element 要添加的元素值
*/
public void addLast(int element) {

// 创建一个新的节点,该节点包含指定的元素值,并其下一个节点引用为null
Node node = new Node(element, null);

// 从头节点开始,找到链表的末尾节点
Node tail = head;
while (tail.getNext() != null) {
tail = tail.getNext();
}

// 将新节点的添加到末尾节点的下一个节点引用,从而将新节点添加到链表的末尾
tail.setNext(node);
}

addFirst

addFirst()方法用于向链表的头部添加一个新的元素,插入的新元素总是位于链表的头部(也就是头节点指向的节点),我们称之为头插法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 在链表的开头添加一个新节点,该节点包含指定的元素值。
* @param element 要添加的元素值
*/
public void addFirst(int element) {

// 创建一个新的节点,该节点包含指定的元素值,并其下一个节点引用为null
Node node = new Node(element, null);

// 将新节点的下一个节点引用设置为当前头节点的下一个节点
node.setNext(head.getNext());

// 将头节点的下一个节点引用更新为新节点,从而将新节点添加到链表的开头
head.setNext(node);
}

可以看到,在链表头部插入元素的步骤如下:

  1. 根据新元素的值,构建一枚新节点
  2. 将新节点指针域置为原先链表中头节点指向的节点
  3. 最后,将头节点指向新节点

移除元素

remove

remove(int index)方法用于删除链表中指定位置index的元素。

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
/**
* 删除链表中指定位置的元素。
*
* @param index 要删除的位置,从0开始计数。
* @return 被删除的元素的值。
* @throws IndexOutOfBoundsException 如果删除位置超出链表范围。
*/
public int remove(int index) throws IndexOutOfBoundsException {
// 检查删除位置是否合法
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("删除位置超出链表范围");
}

// 找到删除位置的前一个节点
Node current = head;
for (int i = 0; i < index; i++) {
current = current.getNext();
}

// 获取要删除的节点
Node removedNode = current.getNext();

// 将前一个节点的next指向被删除节点的next,实现删除
current.setNext(removedNode.getNext());

// 返回被删除节点的值
return removedNode.getElem();
}

removeElement

removeElement(int element)方法用于删除链表中第一个出现的指定元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 删除链表中第一个出现的指定元素。
*
* @param element 要删除的元素。
* @return true 如果成功删除了元素,false 如果链表中不包含该元素。
*/
public boolean removeElement(int element) {
// 找到包含指定元素的节点的前一个节点
Node current = head;
while (current.getNext() != null && current.getNext().getElem() != element) {
current = current.getNext();
}

// 如果找到了节点,进行删除操作
if (current.getNext() != null) {
current.setNext(current.getNext().getNext());
return true; // 成功删除元素
} else {
return false; // 链表中不包含该元素
}
}

removeFirst

removeFirst()方法用于删除并返回链表中的第一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 删除并返回链表中的第一个元素。
*
* @return 被删除的第一个元素的值。
* @throws NoSuchElementException 如果链表为空。
*/
public int removeFirst() throws NoSuchElementException {
// 检查链表是否为空
if (isEmpty()) {
throw new NoSuchElementException("链表为空");
}

// 获取第一个节点
Node firstNode = head.getNext();

// 将头节点的next指向第一个节点的next,实现删除
head.setNext(firstNode.getNext());

// 返回被删除节点的值
return firstNode.getElem();
}

removeLast

removeLast()方法用于删除并返回链表中的最后一个元素。

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
/**
* 删除并返回链表中的最后一个元素。
*
* @return 被删除的最后一个元素的值。
* @throws NoSuchElementException 如果链表为空。
*/
public int removeLast() throws NoSuchElementException {
// 检查链表是否为空
if (isEmpty()) {
throw new NoSuchElementException("链表为空");
}

// 找到倒数第二个节点
Node secondLastNode = head;
while (secondLastNode.getNext().getNext() != null) {
secondLastNode = secondLastNode.getNext();
}

// 获取最后一个节点
Node lastNode = secondLastNode.getNext();

// 将倒数第二个节点的next指向null,实现删除最后一个节点
secondLastNode.setNext(null);

// 返回被删除节点的值
return lastNode.getElem();
}

获取元素

get

get(int index)方法用于返回链表中指定位置的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 返回链表中指定位置的元素。
*
* @param index 要获取元素的位置,从0开始计数。
* @return 指定位置的元素的值。
* @throws IndexOutOfBoundsException 如果指定位置超出链表范围。
*/
public int get(int index) throws IndexOutOfBoundsException {
// 检查指定位置是否合法
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("指定位置超出链表范围");
}

// 找到指定位置的节点
Node current = head.getNext();
for (int i = 0; i < index; i++) {
current = current.getNext();
}

// 返回指定位置的元素值
return current.getElem();
}

getFirst

getFirst()方法用于获取链表中的第一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 获取链表中的第一个元素(头部元素)。
*
* @return 头部元素的值。
* @throws NoSuchElementException 如果链表为空。
*/
public int getFirst() throws NoSuchElementException {
// 检查链表是否为空
if (isEmpty()) {
throw new NoSuchElementException("链表为空");
}

// 获取头节点的下一个节点,即第一个元素
Node firstNode = head.getNext();

// 返回第一个元素的值
return firstNode.getElem();
}

getLast

getLast()方法,用于获取尾部元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 获取链表中的最后一个元素(尾部元素)。
*
* @return 尾部元素的值。
* @throws NoSuchElementException 如果链表为空。
*/
public int getLast() throws NoSuchElementException {
// 检查链表是否为空
if (isEmpty()) {
throw new NoSuchElementException("链表为空");
}

// 找到最后一个节点
Node lastNode = head.getNext();
while (lastNode.getNext() != null) {
lastNode = lastNode.getNext();
}

// 返回最后一个元素的值
return lastNode.getElem();
}

indexOf

indexOf(int element)方法用于查找链表中指定元素从前往后第一次出现的索引。

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
/**
* 查找链表中指定元素从前往后第一次出现的索引。
*
* @param element 要查找的元素。
* @return 指定元素从前往后第一次出现的索引,如果元素不存在则返回 -1。
*/
public int indexOf(int element) {

// 初始化索引为-1,表示元素不存在
int index = -1;

// 从链表的第一个节点开始遍历
Node current = head.getNext();
int currentIndex = 0;

// 遍历链表,查找指定元素从前往后第一次出现的索引
while (current != null) {
if (current.getElem() == element) {
// 如果找到指定元素,更新索引值并跳出循环
index = currentIndex;
break;
}
// 移动到下一个节点
current = current.getNext();
currentIndex++;
}

return index;
}

lastIndexOf

lastIndexOf(int element)方法用于查找链表中指定元素最后一次出现的索引。

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
/**
* 查找链表中指定元素最后一次出现的索引。
*
* @param element 要查找的元素。
* @return 指定元素最后一次出现的索引,如果元素不存在则返回 -1。
*/
public int lastIndexOf(int element) {

// 初始化索引为-1,表示元素不存在
int lastIndex = -1;

// 从链表的第一个节点开始遍历
Node current = head.getNext();
int currentIndex = 0;

// 遍历链表,查找指定元素最后一次出现的索引
while (current != null) {
if (current.getElem() == element) {
// 更新索引值为最后一次找到元素的索引
lastIndex = currentIndex;
}
// 移动到下一个节点
current = current.getNext();
currentIndex++;
}

return lastIndex;
}

数组 VS 链表

特性 数组 链表
存储方式 连续的内存块 分散的节点
访问元素 O(1) O(n)
插入元素 O(n) O(1)
删除元素 O(n) O(1)
大小变化 固定,长度不可变 动态,可灵活扩展
内存使用 较少,预先分配空间 较多,动态分配空间

学习资料


评论
ValineDisqus
avatar
翼同学
知道的越多,不知道的越多
Follow Me
公告
欢迎来到我的个人博客!