《Java数据结构与算法》第二篇:链表(中)——线性表的链式存储结构详解与实践

前言

本文将通过分析一个完整的Java程序来深入理解链表的应用。在Main方法中,我们主要完成了两个核心任务:

第一部分:集合操作演示

创建两个LinkedList对象A和B,分别用数组数据进行初始化

展示链表的基本操作:添加元素、获取大小、遍历输出

实现集合差运算A.removeAll(B),演示链表在集合运算中的应用

第二部分:约瑟夫问题求解

通过用户交互获取约瑟夫问题的参数(总人数n、起始位置k、报数m)

调用JosephusProblem类实现经典的约瑟夫环算法

展示链表在解决实际问题中的强大能力

通过这个具体的代码实例,我们将逐步深入链表的各个知识点。在后续内容中,我们将系统介绍Java.util.*中丰富的链表操作方法,包括添加、删除、查找、遍历等常用功能,以及Collections工具类提供的排序、旋转等高级操作。无论你是初学者还是希望深化理解的开发者,都能从本文中获得实用的知识和技能。

1. 链表的基本概念

1.1 什么是链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

1.2 链表的组成

链表由一系列节点组成,每个节点包含:

数据域:存储数据元素

指针域:存储下一个节点的地址

public class ListNode {
    int data;        // 数据域
    ListNode next;   // 指针域
    
    ListNode(int data) {
        this.data = data;
    }
}

2. 链表的分类

2.1 单链表

每个节点只有一个指针域,指向下一个节点。

2.2 双向链表

每个节点有两个指针域,分别指向前驱和后继节点。

2.3 循环链表

尾节点的指针指向头节点,形成环状结构。

3. 哑头节点详解

3.1 什么是哑头节点

哑头节点(Dummy Head)是在链表第一个实际数据节点之前添加的一个不存储有效数据的节点。

3.2 哑头节点的优势

统一操作逻辑:无论链表是否为空,操作逻辑一致

简化边界处理:避免对头节点的特殊处理

减少空指针异常:提高代码健壮性

3.3 哑头节点的实现

public class Linklist {
    public ListNode head;  // 哑头节点
    
    public Linklist() {
        head = new ListNode(-1);  // 创建哑头节点
        head.next = null;
    }
}

4.代码详解分析

1. Main方法详细分析

让我们从main方法开始,一步一步分析整个程序的执行流程:

public class Main {
    public static void main(String[] args) {
        // 第一部分:使用Java内置LinkedList进行集合操作演示
        LinkedList A = new LinkedList();
        LinkedList B = new LinkedList();
        int[] a = {5, 7, 9, 11, 15, 18, 23, 35, 42, 66};
        int[] b = {2, 4, 7, 9, 13, 18, 45, 66};
        
        // 初始化链表A
        for (int i : a) {
            A.add(i);
        }
        System.out.println("A中有" + A.size() + "个元素");
        System.out.println("A:" + A);
        
        // 初始化链表B
        for (int i : b) {
            B.add(i);
        }
        System.out.println("B中有" + B.size() + "个元素");
        System.out.println("B:" + B);

        // 集合差运算:A - B
        A.removeAll(B);
        System.out.println("减去后A有" + A.size() + "个元素");
        System.out.println("A:" + A);

        // 第二部分:约瑟夫问题求解
        Scanner sc = new Scanner(System.in);

        System.out.print("输入圈中人的个数:");
        int n = sc.nextInt();

        System.out.print("输入开始报数的序号:");
        int k = sc.nextInt();

        System.out.print("报数为m的人出列:");
        int m = sc.nextInt();

        JosephusProblem.josephus(n, m, k);

        sc.close();
    }
}

执行流程分析:

  1. 创建两个LinkedList对象A和B

  2. 用数组数据初始化这两个链表

  3. 演示集合的差运算(A - B)

  4. 通过用户输入获取约瑟夫问题的参数

  5. 调用JosephusProblem类解决问题

2. JosephusProblem类分析

现在来看你实现的约瑟夫问题解决方案:

public class JosephusProblem {
    public static void josephus(int n, int m, int k) {
        LinkedList<Integer> people = new LinkedList<>();
        
        // 初始化人员列表:1到n
        for (int i = 1; i <= n; i++) {
            people.add(i);
        }
        
        // 关键技巧:旋转列表,使第k个人在开头
        Collections.rotate(people, -(k - 1));
        
        int index = 0;
        while (people.size() > 1) {
            // 计算要移除的位置:(当前索引 + m - 1) % 剩余人数
            index = (index + m - 1) % people.size();
            
            int removed = people.remove(index);
            System.out.print(removed + " ");
        }
        
        // 输出最后剩下的人
        System.out.println(people.get(0));
    }
}

算法亮点:

  • 使用Collections.rotate()巧妙处理起始位置

  • 利用取模运算实现循环计数

  • 每次移除后列表自动调整,索引处理正确

算法解析:

为什么需要 %?

因为约瑟夫环是一个圆圈,当数到末尾时需要回到开头。

没有 % 的情况(错误):

// 假设有3个人:[A, B, C],当前在B(索引1),要数3个人
index = 1 + 3 - 1 = 3  // 但列表只有索引0,1,2 → 出错!

有 % 的情况(正确):

// 有3个人,当前在B(索引1),要数3个人
index = (1 + 3 - 1) % 3 = 3 % 3 = 0  // 回到A的位置

% 如何实现"绕圈"效果

% 列表大小的作用是:确保结果永远在 0 到 (列表大小-1) 之间

// 列表有5个人,索引范围:0-4
7 % 5 = 2    // 7 → 2
8 % 5 = 3    // 8 → 3  
9 % 5 = 4    // 9 → 4
10 % 5 = 0   // 10 → 0(绕回开头)
11 % 5 = 1   // 11 → 1
12 % 5 = 2   // 12 → 2

5. Java.util.*中的链表方法详解

5.1 LinkedList类常用方法

5.1.1 添加元素

LinkedList<String> list = new LinkedList<>();

// 基本添加
list.add("A");           // 添加到末尾
list.add(0, "B");        // 添加到指定位置

// 特殊添加
list.addFirst("First");  // 添加到头部
list.addLast("Last");    // 添加到尾部
list.offer("Offer");     // 添加到尾部(队列操作)
list.push("Push");       // 添加到头部(栈操作)

5.1.2 删除元素

// 按位置删除
list.remove(0);          // 删除指定位置
list.removeFirst();      // 删除头部
list.removeLast();       // 删除尾部

// 按值删除
list.remove("A");        // 删除指定元素
list.removeFirstOccurrence("B");  // 删除第一次出现的元素
list.removeLastOccurrence("C");    // 删除最后一次出现的元素

// 清空
list.clear();           // 清空所有元素

5.1.3 获取元素

// 按位置获取
String first = list.get(0);      // 获取指定位置
String firstElem = list.getFirst(); // 获取头部
String lastElem = list.getLast();   // 获取尾部

// 查看但不删除
String peek = list.peek();       // 查看头部
String peekFirst = list.peekFirst(); // 查看头部
String peekLast = list.peekLast();   // 查看尾部

5.1.4 队列和栈操作:这个下一篇再讲嘻嘻。

5.1.5 查找和判断

// 查找位置
int index = list.indexOf("A");   // 第一次出现的位置
int lastIndex = list.lastIndexOf("A"); // 最后一次出现的位置

// 判断存在
boolean contains = list.contains("A"); // 是否包含元素
boolean isEmpty = list.isEmpty();     // 是否为空

// 大小
int size = list.size();               // 元素个数

5.2 Collections工具类方法

5.2.1 排序和旋转

LinkedList<Integer> numbers = new LinkedList<>(Arrays.asList(3,1,4,1,5));

Collections.sort(numbers);           // 排序
Collections.reverse(numbers);        // 反转
Collections.rotate(numbers, 2);      // 旋转
Collections.shuffle(numbers);        // 随机打乱

5.2.2 极值和频率

int max = Collections.max(numbers);  // 最大值
int min = Collections.min(numbers);  // 最小值
int frequency = Collections.frequency(numbers, 1); // 出现次数

5.3 迭代器操作

LinkedList<String> list = new LinkedList<>(Arrays.asList("A","B","C"));

// 正向迭代
ListIterator<String> iterator = list.listIterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

// 反向迭代
ListIterator<String> reverseIterator = list.listIterator(list.size());
while (reverseIterator.hasPrevious()) {
    System.out.println(reverseIterator.previous());
}

6. 链表与顺序表的比较

特性

链表(LinkedList)

顺序表(ArrayList)

访问时间

O(n)

O(1)

插入删除

O(1)

O(n)

内存使用

需要额外指针空间

连续内存空间

适用场景

频繁插入删除

频繁随机访问

总结

通过本文的学习,我们深入探讨了链式存储结构的核心概念和实际应用。从基础的节点定义到复杂的算法实现,从理论分析到代码实践,我们全面掌握了链表这一重要数据结构。

关键收获:

  1. 理解了链表的基本原理和三种分类(单链表、双向链表、循环链表)

  2. 掌握了哑头节点的设计理念和实际优势

  3. 通过约瑟夫问题深入理解了链表在算法中的应用

  4. 学习了Java集合框架中LinkedList的丰富API

  5. 明确了链表与顺序表各自的适用场景

链表作为基础数据结构,其思想会延伸到更多复杂结构中。本文涉及的集合运算、环状处理等技术,为学习更高级的数据结构打下了坚实基础。

致谢

在本文的撰写过程中,我深深感受到技术分享的魅力与价值。感谢每一位坚持阅读到这里的读者,你们的关注是我持续创作的最大动力。

特别感谢Java开源社区和所有为Collections框架贡献智慧的开发者,正是这些优秀的工具让我们能够更加专注于算法和逻辑的实现,而不必重复造轮子。

同时,我要感谢在学习数据结构道路上给予我启发的各位技术前辈,你们的书籍、博客和视频教程为无数学习者点亮了前进的道路。

"独学而无友,则孤陋而寡闻"- 如果本文中任何表述不够准确或存在疏漏之处,恳请各位读者不吝指正。您的每一句建议都是帮助我改进的重要反馈。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值