带头单链表Java实现

带头单链表

一、概念

带头单链表中默认带有一个虚拟头结点dummyHead,不存储有效元素。

二、要点总结

  • 链表中仅创建了虚拟头结点的Node对象 private Node dummyHead = new Node();

    虚拟头结点的属性值val默认为0(不存储有效元素),next引用默认为null

  • 带有虚拟头结点后,头结点也具有了前驱结点,带头单链表中所有的结点都有了前驱结点

    所有结点的插入、删除方式均相同,无需再对头结点进行单独处理,无需单独考虑头结点

  • 插入、删除操作的核心即寻找前驱结点,带头单链表中第一个前驱结点即为虚拟头结点

  • 遍历单链表中的每个元素

    • 遍历带头单链表的每个元素:从第一个有效元素开始(越过虚拟头结点) Node x = dummyHead.next;

    • 遍历不带头单链表的每个元素:从头结点开始 Node x = head;

  • 在带头单链表中根据index寻找结点

    ​ 从虚拟头结点向后走 Node x = dummyHead;

    • 寻找index的前驱结点:从虚拟头结点开始向后走 index 步即到达index的前驱结点

      for(int i = 0; i < index; i++) —— 循环执行index次

    • 寻找index结点:从虚拟头结点开始向后走 (index+1) 步即到达index结点

      for(int i = 0; i < index+1; i++) —— 循环执行(index+1)次

三、代码实现

1. 类结构
//结点类
class Node {
    int val;  //当前结点保存的值
    Node next;  //下一个结点的地址

    public Node(int val) {
        this.val = val;
    }
    public Node(int val, Node next) {
        this.val = val;
        this.next = next;
    }
    public Node() {};
}
//带头单链表
public class SingleLinkedListWithDummyHead {

    private int size;  //链表中有效元素的个数(不包含虚拟头结点),第一个有效元素的索引为0
    private Node dummyHead = new Node();  //虚拟头结点
    ...(增删改查等方法)
}
2. 带头单链表的遍历

【遍历单链表中的每个元素】

  • 遍历带头单链表的每个元素:从第一个有效元素开始(越过虚拟头结点) Node x = dummyHead.next;

  • 遍历不带头单链表的每个元素:从头结点开始 Node x = head;

/** 遍历带头单链表 */
public String toString() {
    String ret = "";
    Node x = dummyHead.next;  //从第一个有效元素开始遍历
    while (x != null) {
        ret += x.val + "->";
        x = x.next;
    }
    ret += "NULL";
    return ret;
}
3. 插入结点
3. 1. 头插法

【不带头、带头单链表的头插法区别】

  • 不带头单链表:对于原头结点存在(链表非空)、原头结点不存在(链表为空)的两种情况,头插操作部分不同

  • 带头单链表:无论链表是否为空,原头结点均可视为存在,只是空链表的原头结点为空结点,而非空链表的原头结点为有效结点,两种情况的头插操作完全相同
    在这里插入图片描述
    在这里插入图片描述

/**(头插)无论原头结点是否存在(链表是否为空),头插操作都一样
    * 若链表不为空,则虚拟头结点next指针指向的原头结点为有效结点head
    * 若链表为空,则虚拟头结点next指针指向的原头结点为空结点(null)
 */
public void addFirst(int val) {
    /*
      1.新建结点
         Node newHead = new Node();
         newHead.val = val;
      2.新结点的next指针指向原头结点(若链表为空,则原头结点为空)
         newHead.next = dummyHead.next;
      3.虚拟头结点的next指针指向新结点
         dummyHead.next = newHead;
     */
    dummyHead.next = new Node(val, dummyHead.next);
    //有效元素增1
    size++;
}
3.2. 指定索引处插入结点

在index处插入结点需找到index的前驱结点。

对于带头单链表,所有的结点包括头结点都有前驱结点,故所有结点的插入操作都相同,无需单独考虑头结点。

【在带头单链表中根据index寻找结点】

从虚拟头结点向后走 Node x = dummyHead;

  • 寻找index的前驱结点:从虚拟头结点开始向后走 index 步即到达index的前驱结点

    for(int i = 0; i < index; i++) —— 循环执行index次

  • 寻找index结点:从虚拟头结点开始向后走 (index+1) 步即到达index结点

    for(int i = 0; i < index+1; i++) —— 循环执行(index+1)次
    在这里插入图片描述

/**(指定index处插入元素)前驱结点指针prev从虚拟头结点开始向后走,需要走index步即找到插入位置的前驱结点 ==> 循环index次
    * 链表中包含了虚拟头结点后,头结点也有了前驱结点,即所有的结点都有了前驱结点,插入、删除的操作均一致,无需再对头结点进行单独处理
 */
public void add(int index, int val) {
    //【传参涉及index就要判断其合法性】允许(index=0)头插、(index=size)尾插
    if(index < 0 || index > size) {
        System.err.println("add index illegal!");
        return;
    }
    //从虚拟头结点开始向后寻找index的前驱结点
    Node prev = dummyHead;
    //走index步即找到prev结点 ==> 循环index次
    for(int i = 0; i < index; i++) {
        prev = prev.next;  //prev向后移动一步
    }
    /*
      1.新建结点
         Node node = new Node();
         node.val = val;
      2.新结点的next指针指向index结点
         node.next = prev.next;
      3.前驱结点的next指针指向新结点
         prev.next = node;
     */
    prev.next = new Node(val, prev.next);
    //有效元素增1
    size++;
}
3.3 尾插法

可直接复用指定索引插入结点的方法add(int index, int val)。尾结点有前驱结点,同其他所有结点的插入操作相同。

/**(尾插法)尾结点同其他结点一样具有前驱结点,可直接复用add(int index, int val)方法 */
public void addLast(int val) {
    add(size, val);
}
4. 删除结点
4.1. 删除指定索引处的结点

删除index处的结点需先找到index的前驱结点。

对于带头单链表,所有的结点包括头结点都有前驱结点,故所有结点的删除操作都相同,无需单独考虑头结点。

/** 判断index索引的合法性 —— 查询、删除、修改,设定为private私有(用户不关心)*/
private boolean rangeCheck(int index) {
    if(index < 0 || index >= size) {  //必须在有效元素的范围内,不允许(index=size)
        System.err.println("index illegal!");
        return false;
    }
    return true;
}

/** 删除指定索引处的结点,返回被删除的结点值,索引越界返回-1 */
public int remove(int index) {
    //【传参涉及index就要判断其合法性】
    if(!rangeCheck(index)) {
        return -1;
    }
    //从虚拟头结点开始向后寻找index的前驱结点
    Node prev = dummyHead;
    //向后走index步即找到前驱结点
    for(int i = 0; i < index; i++) {
        prev = prev.next;
    }
    //找到index的前驱结点,暂存index结点值用于返回
    Node node = prev.next;
    int temp = node.val;
    //删除index结点
    prev.next = node.next;  //前驱结点的next指针指向后驱结点
    node.next = node = null;  //指向后驱结点的next指针置空,将node自身也置空
    size--;
    return temp;
}
4.2 删除所有值为val的结点

1)考虑链表中出现多个连续相邻的val要删除的情况:

设置循环,每次都只删除一个结点,删除后即检查此结点的后继结点是否还是val,若还是val则继续删除此结点,若不是val则向后移动前驱结点指针prev,寻找下一个要删除的val

2)由于带头单链表的所有结点(包括头结点)都有前驱结点,故无需单独考虑头结点

/** 删除所有值为val的结点 */
public void removeByValueAll(int val) {
    //寻找val结点的前驱结点
    Node prev = dummyHead;  //虚拟头结点是链表中的第一个前驱结点
    while (prev.next != null) {  //后方还有结点
        if (prev.next.val == val) {  //循环删除所有值为val的结点
            //删除结点
            Node node = prev.next;
            prev.next = node.next;
            node.next = node = null;
            size--;
        } else {  //prev.next不是待删除结点
            prev = prev.next;  //移动prev指针
        }
    }
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值