自己动手写一个单链表

本文介绍单向链表,它链接方向单向,插入、删除比顺序存储快,查找较慢。文中给出新增、删除节点的图解,展示了单向链表的Java实现及测试代码,还介绍了链表反转、查找中间节点、查找倒数第k个元素等常用操作的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章有不当之处,欢迎指正,如果喜欢微信阅读,你也可以关注我的微信公众号: 好好学java,获取优质学习资源。

一、概述

单向链表(单链表)是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始。

链式存储结构的线性表将采用一组任意的存储单元存放线性表中的数据元素。由于不需要按顺序存储,链表在插入、删除数据元素时比顺序存储要快,但是在查找一个节点时则要比顺序存储要慢

使用链式存储可以克服顺序线性表需要预先知道数据大小的缺点,链表结构可以充分利用内存空间,实现灵活的内存动态管理。但是链式存储失去了数组随机存取的特点,同时增加了节点的指针域,空间开销较大。

二、图解

下图就是最简单最一般的单向链表:

这里写图片描述

新增节点:

将值为element的新节点插入到第index的位置上。

首先要先找到索引为index-1的节点,然后生成一个数据为element的新节点newNode,并令index-1处节点的next指向新节点,新节点的next指向原来index处的节点。
这里写图片描述

删除节点:

删除第index个节点,第index节点是由index-1出的节点引用的,因此删除index的节点要先获取index-1处的节点,然后让index-1出节点的next引用到原index+1处的节点,并释放index处节点即可。

这里写图片描述

三、单向链表的Java实现

下面的程序分别实现了线性表的初始化、获取线性表长度、获取指定索引处元素、根据值查找、插入、删除、清空等操作。

public class LinkList<T> {  
   
    // 定义一个内部类Node,代表链表的节点  
    private class Node {  
   
        private T data;// 保存数据  
        private Node next;// 指向下个节点的引用  
   
        // 无参构造器  
        public Node() {  
        }  
   
        // 初始化全部属性的构造器  
        public Node(T data, Node next) {  
            this.data = data;  
            this.next = next;  
        }  
    }  
   
    private Node header;// 保存头结点  
    private Node tail;// 保存尾节点  
    private int size;// 保存已含有的节点数  
   
    // 创建空链表  
    public LinkList() {  
        header = null;  
        tail = null;  
    }  
   
    // 已指定数据元素创建链表,只有一个元素  
    public LinkList(T element) {  
   
        header = new Node(element, null);  
        // 只有一个节点,header,tail都指向该节点  
        tail = header;  
        size++;  
    }  
   
    // 返回链表的长度  
    public int length() {  
        return size;  
    }  
   
    // 获取指定索引处的元素  
    public T get(int index) {  
        return this.getNodeByIndex(index).data;  
    }  
   
    //获取指定位置的节点  
    private Node getNodeByIndex(int index){  
        if(index < 0 || index > size-1){  
            throw new IndexOutOfBoundsException("索引超出线性表范围");  
        }  
           
        Node current = header;//从header开始遍历  
           
        for(int i=0; i<size && current!=null; i++,current=current.next){  
            if(i == index){  
                return current;  
            }  
        }  
           
        return null;  
    }  
       
    //按值查找所在位置  
    public int locate(T element){  
        Node current = header;  
           
        for(int i=0; i<size && current!=null; i++, current=current.next){  
            if(current.data.equals(element)){  
                return i;  
            }  
        }  
           
        return -1;  
    }  
       
    //指定位置插入元素  
    public void insert(T element, int index){  
       
        if(index < 0 || index > size){  
            throw new IndexOutOfBoundsException("索引超出线性表范围");  
        }  
           
        //如果是空链表  
        if(header == null){  
            add(element);  
        }  
        else{  
            //当index为0时,即在链表头处插入  
            if(0 == index){  
                addAtHead(element);  
            }  
            else{  
                Node prev = getNodeByIndex(index - 1);//获取前一个节点  
                //让prev的next指向新节点,新节点的next指向原来prev的下一个节点  
                prev.next = new Node(element, prev.next);  
                size++;  
            }  
        }  
    }  
   
       
    //在尾部插入元素  
    public void add(T element) {  
           
        //如果链表是空的  
        if(header == null){  
            header = new Node(element, null);  
               
            //只有一个节点,headwe,tail都该指向该节点  
            tail = header;  
        }  
        else{  
            Node newNode = new Node(element, null);//创建新节点  
            tail.next = newNode;//尾节点的next指向新节点  
            tail = newNode;//将新节点作为尾节点  
        }  
           
        size++;  
    }  
       
       
    //头部插入  
    public void addAtHead(T element){  
        //创建新节点,让新节点的next指向header  
        //并以新节点作为新的header  
        Node newNode = new Node(element, null);  
        newNode.next = header;  
        header = newNode;  
           
        //若插入前是空表  
        if(tail == null){  
            tail = header;  
        }  
           
        size++;  
    }  
       
    //删除指定索引处的元素  
    public T delete(int index){  
           
        if(index < 0 || index > size-1){  
            throw new IndexOutOfBoundsException("索引超出线性表范围");  
        }  
           
        Node del = null;  
           
        //若要删除的是头节点  
        if(index == 0){  
            del = header;  
            header = header.next;  
        }  
        else{  
            Node prev = getNodeByIndex(index - 1);//获取待删除节点的前一个节点  
               
            del = prev.next;//获取待删除节点  
               
            prev.next = del.next;  
               
            del.next = null;//将被删除节点的next引用置为空  
        }  
           
        size--;  
        return del.data;  
    }  
       
    //删除最后一个元素  
    public T remove(){  
        return delete(size - 1);  
    }  
       
    //判断线性表是否为空  
    public boolean isEmpty(){  
        return size == 0;  
    }  
       
    //清空线性表  
    public void clear(){  
        //将header,tail置为null  
        header = null;  
        tail = null;  
        size = 0;  
    }  
       
    public String toString(){  
           
        if(isEmpty()){  
            return "[]";  
        }  
        else{  
            StringBuilder sb = new StringBuilder("[");  
            for(Node current = header; current != null; current = current.next){  
                sb.append(current.data.toString() + ", ");  
            }  
               
            int len = sb.length();  
               
            return sb.delete(len-2, len).append("]").toString();  
        }  
           
    }  
}  

四、测试代码

import org.junit.Test;  
   
import com.sihai.algorithm.LinkList;  
   
public class LinkListTest {  
   
    @Test  
    public void test() {  
   
        // 测试构造函数  
        LinkList<String> list = new LinkList("好");  
        System.out.println(list);  
   
        // 测试添加元素  
        list.add("放大");  
        list.add("没");  
        System.out.println(list);  
   
        // 在头部添加  
        list.addAtHead("啦啦啦");  
        System.out.println(list);  
   
        // 在指定位置添加  
        list.insert("膜拜", 2);  
        System.out.println(list);  
   
        // 获取指定位置处的元素  
        System.out.println("第2个元素是(从0开始计数):" + list.get(2));  
   
        // 返回元素索引  
        System.out.println("膜拜在的位置是:" + list.locate("膜拜"));  
        System.out.println("mobai所在的位置:" + list.locate("mobai"));  
   
        // 获取长度  
        System.out.println("当前线性表的长度:" + list.length());  
   
        // 判断是否为空  
        System.out.println(list.isEmpty());  
   
        // 删除最后一个元素  
        list.remove();  
        System.out.println("调用remove()后:" + list);  
   
        // 获取长度  
        System.out.println("当前线性表的长度:" + list.length());  
   
        // 删除指定位置处元素  
        list.delete(3);  
        System.out.println("删除第4个元素后:" + list);  
   
        // 获取长度  
        System.out.println("当前线性表的长度:" + list.length());  
   
        // 清空  
        list.clear();  
        System.out.println(list);  
   
        // 判断是否为空  
        System.out.println(list.isEmpty());  
    }  
   
}  

五、链表相关的常用操作实现方法

1. 链表反转
/**
     * 链表反转
     * 
     * @param head
     * @return
     */
    public Node ReverseIteratively(Node head) {
        Node pReversedHead = head;
        Node pNode = head;
        Node pPrev = null;
        while (pNode != null) {
            Node pNext = pNode.next;
            if (pNext == null) {
                pReversedHead = pNode;
            }
            pNode.next = pPrev;
            pPrev = pNode;
            pNode = pNext;
        }
        this.head = pReversedHead;
        return this.head;
    }
2. 查找单链表的中间节点

采用快慢指针的方式查找单链表的中间节点,快指针一次走两步,慢指针一次走一步,当快指针走完时,慢指针刚好到达中间节点。

/**
     * 查找单链表的中间节点
     * 
     * @param head
     * @return
     */
    public Node SearchMid(Node head) {
        Node p = this.head, q = this.head;
        while (p != null && p.next != null && p.next.next != null) {
            p = p.next.next;
            q = q.next;
        }
        System.out.println("Mid:" + q.data);
        return q;
    }
3. 查找倒数第k个元素

采用两个指针P1,P2,P1先前移K步,然后P1、P2同时移动,当p1移动到尾部时,P2所指位置的元素即倒数第k个元素 。

/**
     * 查找倒数 第k个元素
     * 
     * @param head
     * @param k
     * @return
     */
    public Node findElem(Node head, int k) {
        if (k < 1 || k > this.length()) {
            return null;
        }
        Node p1 = head;
        Node p2 = head;
        for (int i = 0; i < k; i++)// 前移k步
            p1 = p1.next;
        while (p1 != null) {
            p1 = p1.next;
            p2 = p2.next;
        }
        return p2;
    }
4. 对链表进行排序
/**
     * 排序
     * 
     * @return
     */
    public Node orderList() {
        Node nextNode = null;
        int tmp = 0;
        Node curNode = head;
        while (curNode.next != null) {
            nextNode = curNode.next;
            while (nextNode != null) {
                if (curNode.data > nextNode.data) {
                    tmp = curNode.data;
                    curNode.data = nextNode.data;
                    nextNode.data = tmp;
                }
                nextNode = nextNode.next;
            }
            curNode = curNode.next;
        }
        return head;
    }
5. 删除链表中的重复节点
/**
     * 删除重复节点
     */
    public void deleteDuplecate(Node head) {
        Node p = head;
        while (p != null) {
            Node q = p;
            while (q.next != null) {
                if (p.data == q.next.data) {
                    q.next = q.next.next;
                } else
                    q = q.next;
            }
            p = p.next;
        }

    }
6. 从尾到头输出单链表,采用递归方式实现
/**
     * 从尾到头输出单链表,采用递归方式实现
     * 
     * @param pListHead
     */
    public void printListReversely(Node pListHead) {
        if (pListHead != null) {
            printListReversely(pListHead.next);
            System.out.println("printListReversely:" + pListHead.data);
        }
    }
7. 判断链表是否有环,有环情况下找出环的入口节点
/**
     * 判断链表是否有环,单向链表有环时,尾节点相同
     * 
     * @param head
     * @return
     */
    public boolean IsLoop(Node head) {
        Node fast = head, slow = head;
        if (fast == null) {
            return false;
        }
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                System.out.println("该链表有环");
                return true;
            }
        }
        return !(fast == null || fast.next == null);
    }

    /**
     * 找出链表环的入口
     * 
     * @param head
     * @return
     */
    public Node FindLoopPort(Node head) {
        Node fast = head, slow = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast)
                break;
        }
        if (fast == null || fast.next == null)
            return null;
        slow = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
参考资料:
<think>嗯,用户问的是考研复习408时,数据结构第一遍能听懂但代码不出来,这种情况正常吗。首先,我需要理解用户的具体情况。用户可能在听课时理解了概念,比如链表、树、图这些数据结构的基本原理,但实际动手代码的时候遇到了困难。这可能是因为理论知识和实践应用之间存在差距,尤其是在没有足够编程经验的情况下。 接下来,我应该考虑用户可能的背景。用户可能在本科阶段学过数据结构,但编程练习不够,或者之前的学习更侧重于理论而非实际编码。考研408的难度较高,尤其是算法题,需要较强的动手能力。用户的问题反映出理论实践脱节,这在初学者中很常见。 然后,我需要分析问题的原因。可能包括:对算法逻辑理解不深入,缺乏分步骤实现的训练,对编程语言的语法不熟悉,或者没有掌握调试技巧。此外,可能用户听课的时候只是被动接受,没有主动去拆解和复现代码,导致听懂但不会。 接下来,我应该给出具体的建议。比如,建议用户从模仿开始,拆解经典代码,逐步练习,分模块实现,同时多动手调试。还要强调坚持练习的重要性,毕竟编程能力需要积累。另外,可能需要推荐一些练习资源,比如《算法导论》、LeetCode或《代码随想录》等。 还要考虑用户的情绪,给予鼓励,让他们知道这种情况是正常的,避免他们感到沮丧。同时提醒他们不要急于求成,制定合理的学习计划,定期复习,逐步提升。 最后,检查是否有遗漏的点,比如是否要提到时间管理,或者是否需要结合其他科目复习。但根据问题,重点应放在数据结构的代码实践上,所以保持回答的针对性。</think>这种情况**非常正常**,尤其是在初次系统学习数据结构算法的阶段。以下是具体分析和应对建议: --- ### **一、原因分析** 1. **理论到实践的天然鸿沟** - 听课理解的往往是**逻辑框架**(如链表如何连接、树如何遍历),但代码需要**精确控制细节**(指针操作、边界条件、递归终止条件)。 - 例如:理解二叉树的中序遍历逻辑只需1分钟,但出非递归实现需要处理栈、指针、循环终止条件,可能卡壳半小时。 2. **缺乏分步拆解训练** - 课堂代码多为“成品”,未展示**调试过程**和**中间步骤**(如画图分析指针变化、打印中间变量)。 - 例如:合并两个有序链表时,听课时觉得“无非是逐个比较”,实际编码时可能因指针丢失或未处理空链表而崩溃。 3. **对语言特性不熟悉** - 数据结构代码高度依赖**指针/引用**(C/C++)或**对象引用**(Java),若语法基础不牢(如二级指针、结构体内存分配),会放大实现难度。 --- ### **二、针对性解决方案** #### **1. 从“模仿”开始,拆解经典代码** - **步骤**: ① 听课后立即手抄一遍课堂代码(如单链表插入),标注每行作用; ② 盖住代码,根据注释尝试复现; ③ 对比差异,重点分析自己漏掉的边界条件(如头结点为空、插入位置越界)。 - **示例**: ```c // 单链表插入(在位置i插入节点) void InsertNode(LinkList &L, int i, ElemType e) { Node *p = L; int j = 0; while (p && j < i-1) { // 找到第i-1个节点 p = p->next; j++; } if (!p || j > i-1) { // 处理i不合法的情况 printf("位置无效"); return; } Node *s = (Node*)malloc(sizeof(Node)); s->data = e; s->next = p->next; // 关键指针操作顺序! p->next = s; } ``` **常见错误**:未检查`i`的合法性、指针操作顺序颠倒导致链表断裂。 #### **2. 分模块练习,降低认知负荷** - **初级阶段**:先实现**独立功能模块**(如单链表的增删查),再组合成完整算法。 - **进阶训练**: - 固定数据结构的**ADT(抽象数据类型)**,用不同方法实现(如栈可用数组或链表实现); - 对比不同实现的时间/空间复杂度,加深对理论的理解。 #### **3. 必练的“最小核心算法集”** | 数据结构 | 必会代码题 | 练习重点 | |--------------|--------------------------------------------|----------------------------------| | 线性表 | 单链表逆置、双链表删除节点 | 指针操作、边界条件 | | 栈队列 | 用栈实现队列、括号匹配 | 结构封装、状态判断 | | 树 | 二叉树遍历(递归/非递归)、求高度 | 递归思维、栈模拟递归 | | 图 | DFS/BFS、拓扑排序 | 邻接表/矩阵的差异、visited标记 | | 排序查找 | 快速排序、二分查找 | 分治思想、循环终止条件 | #### **4. 调试技巧:打印中间状态** - 在关键位置插入打印语句,观察变量变化: ```c void ReverseList(LinkList &L) { Node *pre = NULL, *cur = L; while (cur) { Node *next = cur->next; // 保存下一个节点 printf("当前节点:%d, next指向:%p\n", cur->data, next); cur->next = pre; // 反转指针 pre = cur; cur = next; } L = pre; } ``` - 通过输出验证:指针是否按预期反转,是否遗漏节点。 --- ### **三、时间规划建议(针对408复习)** 1. **基础阶段(6-8周)**: - 目标:能独立出线性表、树的核心代码(如链表逆置、二叉树遍历) - 每日投入:1小时代码练习 + 30分钟调试分析 2. **强化阶段(4-6周)**: - 目标:攻克图算法(DFS/BFS)和经典排序(快排/堆排序) - 配合真题:练习2010-2020年408算法题,总结命题规律(如近年常考二叉树+递归) 3. **冲刺阶段(2-3周)**: - 限时训练:在30分钟内完成一道中等难度算法题(如LeetCode中等题) - 查漏补缺:针对易错点专项突破(如二级指针、递归出口设计) --- ### **四、心理建设** - **接受“卡壳”是进步的必经之路**:即使清北学生初学链表时也会出死循环。 - **量化进步**:记录每天解决的Bug,例如:“今日修复了双链表删除时未更新前驱节点的问题”。 - **避免完美主义**:第一遍代码允许冗余变量或不优雅的法,后续再优化。 **最后提醒**:408算法题常考**对基础数据结构的灵活应用**而非复杂算法,重点训练将问题转化为已学结构(如用栈模拟递归、用哈希表优化查找)的能力。坚持2-3个月针对性练习后,代码能力会有质的提升。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值