数据结构之单/双向链表

一、单向链表

介绍

  • 链表是逻辑有序,空间无序的。
    如下图所示,这是一个带头节点的链表在内存中的存储结构。相邻的链表节点存储内存并不一定在一起,而是通过next去指向下一个节点所在的位置
    在这里插入图片描述
  • 链表是以节点的方式来存储,是链式存储
  • 每个节点包含 data 域, next 域:指向下一个节点
  • 如图:发现链表的各个节点不一定是连续存储
  • 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定

实例

  • 使用带 head 头的单向链表实现 –根据添加人物的排名对水浒英雄人物的增删查操作(该对象属性包括:姓名,排名,昵称,以及其指向的next节点对象)

首先我们来分析增加如何实现:

我们需要根据传入人物的排名将该人物插入到对应的链表中间(如果有这个排名,则添加失败,并给出提示),思路分析
在这里插入图片描述
我的理解:假设要将数据2插入到数据1和4之间,那我们应该先将数据2的下个节点设置为数据4,再将数据1的下个节点设置为数据2。可以理解为一个人拉着另外一个要掉下悬崖人的手,现在要再这两人中间再加上一个人,那肯定让下面那个人连上新加的人的手,再让新加人的手连上一开始上面的人,如果反之的下面的人就没有人跟他连接了,他不就直接掉下去了嘛。详情见代码

再来分析一下如何删除一个指定节点
1.我们需要找到需要删除的这个节点的前一个节点temp
2.temp.next = temp.next.next【直接将需要删除的前一个节点指向下下个节点,绕过被删除节点,这样java的垃圾回收器就回去回收它】
在这里插入图片描述

代码

public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //测试
        //创建节点
        HeroNode hero1 = new HeroNode(1,"宋江","及时雨");
        HeroNode hero2 = new HeroNode(2,"卢俊义","玉麒麟");
        HeroNode hero3 = new HeroNode(3,"吴用","智多星");
        HeroNode hero4 = new HeroNode(4,"林冲","豹子头");

        //创建链表
        SingleLinkedList singleLinkedList = new SingleLinkedList();

        //加入
        singleLinkedList.addByOrder(hero1);
        singleLinkedList.addByOrder(hero3);
        singleLinkedList.addByOrder(hero4);
        singleLinkedList.addByOrder(hero2);
        //修改链表之前的情况
        singleLinkedList.list();

        //从链表中删除一个节点
        singleLinkedList.del(2);
        //删除链表之后的情况
        System.out.println("======华丽的分割线=======");
        singleLinkedList.list();
    }
}

/**
 * 定义SingleLinkedList 管理英雄
 */
class SingleLinkedList{
    /**
     * 初始化头结点,不存储具体数据,注意:头结点不要动,因为我们需要根据头结点去找到下一个节点
     */
    private HeroNode head = new HeroNode(0,",","");

    /**
     *第二种在添加英雄时,根据排名将英雄插入到指定位置
     * 如果有这个排名,则添加失败,并给出提示
     */
    public void addByOrder(HeroNode heroNode) {
        //因为头结点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
        //因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了
        HeroNode temp = head;
        //flag标志添加的编号是否存在,默认为false
        boolean flag = false;
        while(true) {
            //说明temp已经在链表的最后面
            if(temp.next == null){
                break;
            }
            if(temp.next.no>heroNode.no) {
                //位置找到了,因为temp的下一个节点大于传入值得节点
                break;
            }else if(temp.next.no == heroNode.no){
                //说明希望添加的heroNode已经存在
                flag = true;
                break;
            }
            //后移,继续遍历当前链表
            temp = temp.next;
        }
        //判断flag的值
        if(flag){
            //不能添加,说明编号已经存在
            System.out.printf("准备插入的英雄编号 %d 已经存在\n",heroNode.no);
        }else{
            //插入到链表中
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    /**
     * 显示列表【遍历】
     */
    public void list() {
        //判断链表是否为空
        if(head.next == null){
            System.out.println("链表为空");
            return;
        }
        //因为头节点,不能动,因此我们需要一个辅助变量来遍历
        HeroNode temp = head.next;
        while (temp != null) {
            //判断是否到链表最后
            //输出节点信息
            System.out.println(temp);
            //将temp后移
            temp = temp.next;
        }
    }

    /**
     * 删除节点
     * 思路
     *      1.head 不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
     *      2.说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较
     */
    public void del(int no) {
        HeroNode temp = head;
        //记录该值是否能被删除
        boolean flag = false;
        while(true){
            if(temp.next == null){
                //查到最后一个节点都没有我们要删除的对象
                break;
            }
            if(temp.next.no == no) {
                //找到待删除节点的前一个节点:temp
                flag = true;
                break;
            }
            //temp后移
            temp = temp.next;
        }
        //判断flag从而得知能否删除该节点
        if(!flag) {
            System.out.printf("不跟删除节点为 %d 的值",no);
        }else{
            temp.next = temp.next.next;
        }
    }
}

/**
 *定义HeroNode,每个HeroNode对象就是一个节点
 */
class HeroNode{
    public int no;
    public String name;
    public String nickname;
    /**
     * 指向下一个节点
     */
    public HeroNode next;

    /**
     * 构造器
     */
    public HeroNode(int no, String name, String nickname) {
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }

    /**
     * toString
     */
    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickname='" + nickname +
                '}';
    }
}

面试题

一、查询链表有效节点的个数
思路 :遍历节点后的个数即可

二、查找单链表中的倒数第index个结点 【新浪面试题】
1、先遍历得到 有效数据个数(总长度–不包括头)
2、有效个数 - index = 需要从头查找的次数n
3、遍历链表n次即可得到倒数第个节点

三、 单链表的反转【腾讯面试题,有点难度】
思路:
1.先定义一个节点 reverseHead = new HeroNode()
2.定义一个next指向遍历中原链表的下一个节点,如果链表为1-2-3-4,那么遍历该链表的第一次next的值就为2-3-4,而不单单是2
3.从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
4.将原来的链表指向倒转后的链表head.next = reverseHead.next
5.对于初学者来说可能有点难理解,debug多跑几次,多想想

在这里插入图片描述

在这里插入图片描述

四、从尾到头打印单链表 【百度】
思路:
1.先反转链表,再打印【缺点:破坏链表结构,建议】
2.使用栈先进后出的特性,遍历链表,将各个节点压入栈,再遍历栈,这样最近进栈的第一个节点就最后打印出
在这里插入图片描述

代码

 /**
     * 题目一 查询有效节点的个数
     */
    public static int getLength(HeroNode head) {
        int length = 0;
        if(head.next == null) {
            //空链表
            return length;
        }
        //定义一个辅助变量
        HeroNode cur = head.next;
        while(cur != null){
            length++;
            cur = cur.next;
        }
        return length;
    }

    /**
     * 题目二:查找单链表中的倒数第index个结点 【新浪面试题】
     *思路:
     * 1、先遍历得到 有效数据个数(总长度--不包括头)
     * 2、有效个数 - index = 需要从头查找的次数n
     * 3、遍历链表n次即可得到倒数第个节点
     */

    public static HeroNode findLastIndexNode(HeroNode head,int index){
        // 判空
        if (head.next == null) {
            return null;
        }
        //链表总长度
        int size = getLength(head);
        //对index的合理性做出校验
        if (index <= 0 || index > size) {
            // 查找的位置不能大过总长
            return null;
        }

        // 定义给辅助变量, for 循环定位到倒数的index
        HeroNode cuc = head.next;
        //如果链表长度为4,查询倒数第一个,cuc应该.next三次
        for (int i = 0; i < size - index; i++) {
            cuc = cuc.next;
        }
        return cuc;
    }

    /**
     * 题目三:单链表的反转【腾讯面试题,有点难度】
     */
    public static void reversetList(HeroNode head) {
        // 若链表为空,或者只有一个则不需要反转
        if (head.next == null || head.next.next == null) {
            return;
        }
        // 定义一个辅助指针,用户遍历原始未反转的那个链表
        HeroNode cuc = head.next;
        // 用于动态指向 当前节点【cuc】的 下一个节点
        HeroNode next = null;
        // 新链表(反转后的)的链表头
        HeroNode reversetHead = new HeroNode(0, "", "");
        // 思路: 遍历原始链表,没遍历一个节点将其取出,并放在新链表头(reversetHead)的后面的最前端
        // 每一个新节点,都会取代旧的,与新链表头牵手
        while (cuc != null) {
            // 暂时先保存当前节点的下一个节点,用于后移
            next = cuc.next;
            // 将要 遍历出来的新节点,指向 新链表头 后面的最前端(将情敌的手,从她手上拨开)
            cuc.next = reversetHead.next;
            // 将 cuc 连接到 新的链表头后面的最前端(她的手和你牵了)
            reversetHead.next = cuc;
            // cuc 后移,换下一个情敌来打你
            cuc = next;
        }
        // 将head.next 指向 reverseHead.next , 实现单链表的反转
        // 新链表(她)池塘养的鱼和旧链表头(她的闺蜜)共享
        head.next = reversetHead.next;
    }

    /**
     * 题目四:从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】
     * 思路:
     *      1.我们可以先将链表倒转,再遍历。缺点:破坏链表原有结构
     *      2.将链表遍历入栈,再遍历栈。栈特点:先进后出
     */
    public static void stackPrint(HeroNode head) {
        //如果链表为空直接return
        if(head.next == null){
            System.out.println("链表为空");
            return;
        }
        //创建一个栈,将各个节点压入栈
        Stack<HeroNode> stack = new Stack<>();
        //遍历链表将节点压入栈
        while(head.next != null) {
            stack.push(head.next);
            //链表指针后移
            head = head.next;
        }
        //遍历stack,将栈中的节点打印,弹栈
        while(!stack.isEmpty()){
            System.out.println(stack.pop());
        }
    }

双向链表

区别

1、单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找,从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点。
2、单向链表不能自我删除,需要靠辅助(前一个)节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp.next = temp.next.next 待删除节点的前一个节点
3.双向链表:增加删除节点复杂,需要多分配一个指针存储空间。
4.单向链表:单个结点创建非常方便,普通的线性内存通常在创建的时候就需要设定数据的大小,结点的访问方便,可以通过循环或者递归的方法访问到任意数据。

双向链表的CURD

在这里插入图片描述
1.添加,还是根据传入HeroNode的编号属性进行添加,插在第一个比传入的编号值大的节点前面,详情解释见注释
2.修改,根据传入编号修改属性
3.删除,找到要删除的节点,让其上一个节点的next指针指向被删除节点的下一个节点,让被删除节点的下一个节点的pre指向被删除节点的上一个节点
4.查询,遍历节点

public class DoubleLinkedListDemo {
    public static void main(String[] args) {
        HeroNode2 heroNode = new HeroNode2(1,"宋江","及时雨");
        HeroNode2 heroNode2 = new HeroNode2(2,"吴用","智多星");
        HeroNode2 heroNode3 = new HeroNode2(3,"豹子头","林冲");
        HeroNode2 heroNode4 = new HeroNode2(4,"纪枫","大枫");
        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
        doubleLinkedList.addByOrder(heroNode);
        doubleLinkedList.addByOrder(heroNode2);
        doubleLinkedList.addByOrder(heroNode4);
        doubleLinkedList.addByOrder(heroNode3);
        doubleLinkedList.list();
        

    }
}

class DoubleLinkedList{
    /**
     * 初始化头结点,不存储具体数据,注意:头结点不要动,因为我们需要根据头结点去找到下一个节点
     */
    private HeroNode2 head = new HeroNode2(0,",","");

    public HeroNode2 getHead() {
        return head;
    }
    /**
     * 显示列表【遍历】
     */
    public void list() {
        //判断链表是否为空
        if(head.next == null){
            System.out.println("链表为空");
            return;
        }
        //因为头节点,不能动,因此我们需要一个辅助变量来遍历
        HeroNode2 temp = head.next;
        while (temp != null) {
            //判断是否到链表最后
            //输出节点信息
            System.out.println(temp);
            //将temp后移
            temp = temp.next;
        }
    }

    /**
     * 第二种在添加英雄时,根据排名将英雄插入到指定位置
     */
    public void addByOrder(HeroNode2 heroNode) {
        //定义变量,能添加时true,不能添加false
        boolean flag = true;
        //定义变量
        HeroNode2 temp = head;
        while(true) {
            if(temp.next == null){
                break;
            }
            if(temp.next.no>heroNode.no) {
                break;
            }else if(temp.next.no == heroNode.no){
                flag = false;
                System.out.println("该节点已存在,不可添加");
            }
            temp = temp.next;
        }

        //是否能插入该节点
        if(flag){
            //插入节点的向下指针指向比它大的数字
            heroNode.next = temp.next;
            //比他大的那个节点的pre节点要指向插入节点
            //判断该节点下一个节点是否为空,空的话就不用设置下一个节点的pre指针
            //这两步就是将插入节点与它后面的那个节点连上
            if(temp.next != null){
                temp.next.pre = heroNode;
            }

            //将插入节点的pre连上要插入两个节点中间的前一个节点
            heroNode.pre = temp;
            //同理前一个节点的next指针指向插入节点
            temp.next = heroNode;
        }
    }

    /**
     * 修改一个节点【单双向链表修改节点的方法一样】
     */
    public void update(HeroNode2 heroNode) {
        //判断被修改的节点是否为空
        if(head.next == null) {
            System.out.println("链表为空");
            return;
        }
        //定义辅助变量
        HeroNode2 temp = head;
        //表示是否找到该节点
        boolean flag = false;
        //遍历链表根据传入节点的no得到被修改的节点
        while(true){
            if(temp.next == null ){
                System.out.println("没有找到被修改节点");
                break;
            }
            if(temp.next.no == heroNode.no){
                //找到了
                flag = true;
                break;
            }
            temp = temp.next;
        }

        //修改节点
        if(flag){
            temp.next.name = heroNode.name;
            temp.next.nickname = heroNode.nickname;
        }else{
            System.out.println("没有找到该节点");
        }
    }

    /**
     * 删除一个节点
     */
    public void del(HeroNode2 heroNode) {
        //判断链表是否为空
        if(head.next == null){
            System.out.println("链表为空");
            return;
        }
        //定义辅助变量,指向节点本身
        HeroNode2 temp = head.next;
        //定义变量,用于确定是否找到被删除节点
        boolean flag = false;
        //遍历节点,寻找被删除节点
        while(true) {
            //到链表最后了
            if(temp == null){
                System.out.println("没有找到待删除节点");
                break;
            }

            //已找到被删除节点
            if(temp.no == heroNode.no){
                flag = true;
            }
            temp = temp.next;
        }

        //判断是否找到待删除节点
        if (!flag){
            System.out.println("没有找到待删除节点");
        }else{
            //将待删除节点的前一个节点的next指向待删除节点的next
            heroNode.pre.next = heroNode.next;
            //判断待删除节点是否处于节点最后
            if(heroNode.next != null){
                heroNode.next.pre = heroNode.pre;
            }
        }
    }
}

class HeroNode2{
    public int no;
    public String name;
    public String nickname;
    /**
     * 指向下一个节点,默认为空
     */
    public HeroNode2 next;

    /**
     * 指向上一个节点,默认为空
     */
    public HeroNode2 pre;

    @Override
    public String toString() {
        return "HeroNode2{" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }

    public HeroNode2(int no, String name, String nickname) {
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
数据结构课程设计中,C++双向链表是一个常见的数据结构之一。它是一种线性数据结构,由多个节点组成,每个节点包含两个指针,一个指向前一个节点,一个指向后一个节点。双向链表相比于单向链表,可以实现双向遍历。 在C++中,可以通过定义一个双向链表类来实现双向链表的功能。以下是一个简单的C++双向链表的实现示例: ```cpp #include <iostream> // 双向链表节点定义 class Node { public: int data; Node* prev; Node* next; }; // 双向链表类定义 class DoublyLinkedList { private: Node* head; // 头节点指针 public: DoublyLinkedList() { head = nullptr; } // 在链表头部插入节点 void insertAtHead(int value) { Node* newNode = new Node(); newNode->data = value; newNode->prev = nullptr; newNode->next = head; if (head != nullptr) { head->prev = newNode; } head = newNode; } // 在链表尾部插入节点 void insertAtTail(int value) { Node* newNode = new Node(); newNode->data = value; newNode->next = nullptr; if (head == nullptr) { newNode->prev = nullptr; head = newNode; return; } Node* temp = head; while (temp->next != nullptr) { temp = temp->next; } temp->next = newNode; newNode->prev = temp; } // 打印链表元素 void printList() { Node* temp = head; while (temp != nullptr) { std::cout << temp->data << " "; temp = temp->next; } std::cout << std::endl; } }; int main() { DoublyLinkedList dll; dll.insertAtHead(3); dll.insertAtHead(2); dll.insertAtHead(1); dll.printList(); // 输出:1 2 3 dll.insertAtTail(4); dll.insertAtTail(5); dll.printList(); // 输出:1 2 3 4 5 return 0; } ``` 以上是一个简单的C++双向链表的实现示例。你可以通过调用`insertAtHead`和`insertAtTail`方法来插入节点,通过调用`printList`方法来打印链表元素。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值