数据结构之——》链表

一、链表概念介绍

链表(Linked List)和数组类似,是一种线性的数据结构,而线性结构又分为顺序存储结构和链式存储结构,因链表虽然逻辑上是连续的,但是在物理存储上是非连续、非顺序的,故又称之为链式存储结构
链表是以节点来存储数据的,节点包含数据域指向域;节点后面的节点称为后继节点,节点前面的节点称为前置节点

  • 有无头节点优缺点:
    作用:头节点作用:头节点是为了对链表建立、删除、逆向的时候操作更统一,不用专门对第一个元素或最后一个元素进行单独处理。
    优点:在于链表有头节点虽然浪费空间,但易理解,边界好处理,不易出错,代码简单;
    缺点:相反无头节点,省空间,难理解,边界不易处理,代码稍复杂。
image.png

常用的链表种类有:单链表、双向链表、循环链表、双向循环链表

二 、单链表(single linked list)

单链表顾名思义,每个节点都是单向联系的,第一个节点链接第二个节点,第二个节点链接第三个节点.....,以此形成一个单链表。

单链表缺点:查找方向只能是从头节点开始。当删除某一个节点时需要靠辅助节点,不能自我删除。

链表实现思路:创建一个Node类作为链表中的节点,类中声明一个value变量(数据域),作用是存放数据;再将Node类声明为next成员变量(指针域),作用是存放下一个节点的引用。当Node节点实例化之后,每个节点将会散列在内存中,刚好可以利用Node类中的next变量来将一个个散列的节点连接起来。

单链表节点类:

package linkedlist;

public class HeroNode {
   //数据域,存放数据
    public String value;
    // 指针域,指向下一个节点
    public HeroNode next;

    public HeroNode() {}

    public HeroNode(String value) {
         this.value = value;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "value='" + value +
                '}';
    }
}

【1】last节点新增
思路:循环遍历链表节点,找到尾部节点,将尾部节点的指向域指向新插入的节点。

    //添加节点到单向链表
    public void add(HeroNode heroNode) {
        // 1.循环遍历链表,因为遍历链表是需要从头节点一个一个往后遍历的,
        //所以需要一个临时变量用于存放下一次要遍历的节点。
        HeroNode temp = headNode;
        while (true) {
            //2.因为尾部节点是最后一个节点,所以它的指向域将会是null,
            //那么可以通过判断尾部节点的指向域是否为null,如果为null,则表示遍历结束。
            if (temp.next == null) {
                break;
            }
            // 3.每遍历一次将下一个节点的指向域赋给零时变量,循环遍历完成后,temp将会获得尾节点
            temp = temp.next;
        }
        //4.尾部节点的指向域指向新插入的节点。
        temp.next = heroNode;
    }

【2】节点修改
思路:循环遍历链表节点,找到需要修改的节点,然后修改即可。

    // 节点修改
    public void update(String oldValue,String newValue){
        //判断链表是否为空
        if (headNode.next == null) {
            System.out.println("链表为空!");
            return;
        }
        HeroNode temp=headNode;
        boolean flag=false;

        while (true) {
            //当到达最后一个元素或者找到需要修改的元素都终止遍历
            if (temp.next == null) {
                break;
            }
            // 判断当前遍历的节点是否是需要修改的节点
            if (oldValue.equals( temp.value)){
                flag=true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
             temp.value = newValue;
        }else {
            System.out.println("未找到需要修改的元素!");
        }
    }

【3】节点删除

image.png

思路:元素删除的思路和新增有点相似;从头节点开始遍历,判断后继节点是否为200节点,如果是200则将200节点的指向域取出来,赋给200节点的前置节点的指向域。

    // 删除节点
    public void remove(String value){
        HeroNode temp = headNode;
        boolean delFlag=false;
        // 1. 遍历链表中所有节点
        while (true) {
            // 2. 如果下一个指向域为空,则表示链表遍历结束
            if (temp.next == null) {
                break;
            }
            // 3. 判断下一个节点是否是需要删除的节点
            if (value.equals(temp.next.value)) {
                delFlag=true;
                break;
            }
            //4. 遍历一个之后,移动指针
            temp = temp.next;
        }
        //5. 删除节点
        if (delFlag) {
            // 将被删除节点的下一个节点和被删除节点的上一个节点的指针域挂钩
            temp.next=temp.next.next;
        }else {
            System.out.println("需要删除的元素未找到,删除失败!");
        }
    }

【4】节点遍历

    //遍历链表
    public void list() {
        //判断链表是否为空
        if (headNode.next == null) {
            System.out.println("链表为空!");
            return;
        }
        HeroNode temp = headNode;
        // 遍历链表中所有节点
        while (true) {
            // 如果下一个指向域为空,则表示链表遍历结束
            if (temp == null) {
                break;
            }
            System.out.println(temp);
            //遍历一个之后,移动指针
            temp = temp.next;
        }
    }

【5】获取链表有效节点个数

    // 获取链表有效节点个数
    public int getLength(){
        //判断链表是否为空
        if (headNode.next == null) {
            System.out.println("链表为空!");
            return 0;
        }
        // 统计有效节点个数
        int size=0;
        // 头节点不是有效节点,所以从头节点的后继节点开始遍历
        HeroNode temp = headNode.next;

        // 遍历链表中所有节点
        while (temp!=null) {
            size++;
            //遍历一个之后,移动指针
            temp=temp.next;
        }
        return size;
    }

【6】获取链表倒数第i个节点
查找倒数第i个元素的公式:length-index=i

    // 获取链表倒数的第i个节点
    public HeroNode findLastIndexNode(int index) {
        //判断链表是否为空
        if (headNode.next == null) {
            return null;
        }
        // 获取链表长度
        int length = getLength();

        //如果小于0,或者大于链表的长度则不存在这个索引
        if (index <= 0 || index > length) {
            return null;
        }
        // 头节点不是有效节点,不遍历,headNode.next是跳过头节点
        HeroNode temp = headNode.next;
        // 从末尾取节点的公式:length-index=i
        // 循环i次,当循环走完后,temp将会存放i节点的后继节点,最后return temp;
        for (int i = 0; i < length-index; i++) {
            temp=temp.next;
        }
        return temp;
    }

【7】链表反转
链表反转过后链表的数据结构也将发生变化。
思路:

  1. 遍历需要反转的链表,用新的链表将需要反转的节点链接,
  2. 每次遍历时让新节点的指向域指向新链表头节点的指向域,新链表的头节点指向域每次循环都会变。第二次遍历的关系指向: 200(新节点)--》100(新链表头节点指向域),
  3. 新链表的头节点指向新节点。第二次遍历结束关系的关系指向:头节点--》200--》100


    image.png
    // 使用头插法反转单链表
    public void reversal() {
        //如果链表为空,或者只有一个节点都不需要反转
        if (headNode.next == null || headNode.next.next == null) {
            return;
        }
        //1. 需要准备的变量:oldNode旧的链表、newNode新的链表、next一个临时节点变量
        HeroNode oldNode = headNode.next;
        HeroNode next = null;
        HeroNode newNode = new HeroNode();

        while (oldNode != null) {
            //2. 循环遍历旧链表的节点,oldNode.next移动节点时不要直接覆盖,赋给next保存起来。
            next = oldNode.next;
            //3. 这时oldNode.next的指向域是空的,再把newNode.next链表的指向域赋给oldNode.next临时保存
            //将newNode.next节点赋给oldNode.next的指向域,这样每次新插入的节点就和之前已经插入的节点衔接,
            //然后就是第4步:将oldNode.next赋给newNode.next的指向域,反转完成
            oldNode.next = newNode.next;
            //4.这时newNode.next的指向域是空的,将当前节点赋给newNode.next
            newNode.next = oldNode;
            //temp往后移
            oldNode = next;
        }

        // 修改headNode节点的指向域,将reversalHead节点的指向域付给headNode节点的指向域
        headNode.next = newNode.next;
    }

二 、双向链表

【1】双向链表介绍
双向链表与单向链表不同的是单向链表每个节点只能指向后继节点,而双向链表的节点不仅指向后继节点还指向前置节点,形成一个双向的关系。
双向链表共有三部分:一个数据域,一个pre指向域,一个next指向域。

image.png

【2】双向链表完整实现代码
大部分功能的实现思路和单向链表没有区别。

package linkedlist;

// 双向链表
public class DoubleLinkedList {
    class Node {
        public String value;
        // 指向下一个节点
        public Node next;
        //指向上一个节点
        public Node pre;

        public Node() {
        }

        public Node(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "value='" + value + '\'' +
                    '}';
        }
    }

    //头节点
    Node headNode = new Node();

    //遍历链表
    public void list() {
        //判断链表是否为空
        if (headNode.next == null) {
            System.out.println("链表为空!");
            return;
        }
        Node temp = headNode.next;
        // 遍历链表中所有节点
        while (temp != null) {
            System.out.println(temp);
            //遍历一个之后,移动指针
            temp = temp.next;
        }
    }

    // 节点修改
    public void update(String oldValue, String newValue) {
        System.out.println();
        //判断链表是否为空
        if (headNode.next == null) {
            System.out.println("链表为空!");
            return;
        }
        // 从头节点之后开始遍历
        Node temp = headNode;
        boolean flag = false;

        //当到达最后一个元素或者找到需要修改的元素都终止遍历
        while (temp != null) {
            // 判断当前遍历的节点是否是需要修改的节点
            if (oldValue.equals(temp.value)) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        // 修改属性
        if (flag) {
            temp.value = newValue;
        } else {
            System.out.println("未找到需要修改的元素!");
        }
    }

    // 测试双向链表功能
    public static void main(String[] args) {
        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
        doubleLinkedList.add("一石二鸟");
        doubleLinkedList.add("二话不说");
        doubleLinkedList.add("七七八八");
        doubleLinkedList.add("四舍五入");
        doubleLinkedList.list();

        doubleLinkedList.update("七七八八", "三心二意");
        doubleLinkedList.list();
        System.out.println();

        doubleLinkedList.remove("四舍五入");
        doubleLinkedList.list();
    }
}

【3】新增节点思路
双向链表和单项链表的节点新增不同之处在于双向链表还多了一个pre指向域,所以当链表新增节点时,既旧节点的next域要指向新节点,新节点的pre域也要指向上一个节点。

    // 链表节点新增
    public void add(String str) {
        Node newNode = new Node(str);
        // 因为头节点不能动,所以需要一个临时变量
        Node temp = headNode;
        //遍历链表的每一个节点,当next等于null时表示链表已经遍历到了最后一个节点
        while (temp.next != null) {
            // 每遍历一次将下一个节点的指向域赋给零时变量,while变量完之后,temp将会获得尾节点
            temp = temp.next;
        }
        // 每遍历一次将下一个节点的指向域赋给零时变量,while变量完之后,temp将会获得尾节点
        temp.next = newNode;
        // 将链表末尾节点赋给新节点的pre指向域
        newNode.pre = temp;
    }

【4】删除节点思路
单向链表删除节点时的做法:判断当前节点的下一个节点是否为需要删除的节点,如果是则改变当前节点的next指向域为被被删除节点的下一节点,单向链表只需要修改next指向域。

双向链表删除节点时的做法:判断当前节点是否为需要删除的节点,如果是那我们既要修改上一个节点的next指向域,还要修改下一个节点的pre指向域,因为当前节点链接了上一个节点和下一个节点,所以我们可以直接修改上一个节点next的指向域为被删除节点的next指向域;可以直接修改下一个节点的pre指向域为被删除节点的pre指向域。

    // 删除节点
    public void remove(String value) {
        Node temp = headNode;
        boolean delFlag = false;
        // 遍历链表中所有节点
        while (true) {
            if (temp==null) {
                break;
            }
            // 判断下一个节点是否是需要删除的节点
            if (value.equals(temp.value)) {
                delFlag = true;
                break;
            }
            //遍历一个之后,移动指针
            temp = temp.next;
        }
        if (delFlag) {
            // 修改被删除节点的上一个节点的next指向域为当前节点的next域
            temp.pre.next = temp.next;
            // 避免空指针异常
            if (temp.next != null) {
                // 修改被删除节下一个节点的pre指向域为当前节点的pre域
                temp.next.pre = temp.pre;
            }
        } else {
            System.out.println("需要删除的元素未找到,删除失败!");
        }
    }

三、单向环形链表(Circular LinkedList)

【1】循环链表/环形链表
循环链表( Circular LinkedList)是另一种形式的链式存储结构其特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。

package linkedlist;

public class Josepfu {
    public static void main(String[] args) {
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        circleSingleLinkedList.add(5);
        circleSingleLinkedList.show();
    }
}

//创建一个环形单链表
class CircleSingleLinkedList {
    //创建一个first节点,当前没有编号
    private Node first = null;

    //添加元素,形成一个环形链表
    public void add(int nums) {
        if (nums < 1) {
            System.out.println("nums值不正确!");
            return;
        }
        //辅助指针,帮助构建环形链表,临时保存每一次的最后一个节点
        Node curNode = null;
        // 循环创建节点
        for (int i = 1; i <= nums; i++) {
            // 每次循环创建一个node对象
            Node node = new Node(i);
            if (i == 1) {
                // node是第一个节点,将第一个节点赋给first
                first = node;
                // 设置首节点的指向域,因为当前只有一个节点所以首节点的next域指向自己-----环形链表就此形成
                first.setNext(first);
                // 将first赋给curNode作为链表的最后一个元素临时保存,下一次新增则可以直接设置curNode的next指向域
                curNode = first;
            } else {
                // 设置链表中最后一个节点的next指向域,指向新的节点
                curNode.setNext(node);
                //设置新节点的next指向域,指向首节点------环形链表形成
                node.setNext(first);
                // 刷新curNode保存的最后一个节点,下一次新增则可以直接设置curNode的next指向域
                curNode = node;
            }
        }
    }

    //遍历元素
    public void show() {
        if (first == null) {
            System.out.println("链表中没有元素,遍历失败!");
            return;
        }
        Node curNode = first;
        while (true) {
            System.out.printf("小孩的编号%d\n", curNode.getNo());
            // 环形链表判断链表遍历结束的条件有所不同,结束条件为:因为环形链表首尾相接,
            // 所以我们需要判断遍历的下一个节点是不是首节点,如果是则表示遍历完成,否则继续遍历
            if (curNode.getNext() == first) {
                break;
            }
            // 获取下一个节点
            curNode = curNode.getNext();
        }
    }
}

//单向链表
class Node {
    private int no;
    private Node next;

    public Node(int no) {
        this.no = no;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}

【2】约瑟夫问题
约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。

   /*
     * 根据用户的输入,计算出小孩(节点)出圈顺序
     * startNo:表示从第几个节点开始数数
     * count:表示数几下
     * nums:表示最初环形链表中最开始有几个节点

     */
    public void josephRing(int startNo, int count, int nums) {
        //先对数据进行校验
        if (first == null || startNo < 1 || startNo > nums) {
            System.out.println("参数有误,请重新输入");
            return;
        }

        //创建一个辅助指针helper(指向最后一个节点)
        Node helper = first;
        // 这个while循环为了获取尾部节点
        while (true) {
            if (helper.next == first) { //说明helper指向了最后一个节点
                break;
            }
            helper = helper.next;
        }

        //报数之前,先让first和helper移动k-1次(不一定是从第一个人开始报数的,可能是从第k个人开始报数的)
        for (int i = 0; i < startNo - 1; i++) {
            first = first.next;
            helper = helper.next;
        }

        //当小孩报数时,让first 和helper指针同时的移动count-1次
        //进行循环,直到只剩下一个节点
        while (true) {
            if (helper == first) { //只剩最后一个节点
                break;
            }
            //让first 和helper指针同时的移动count -1次
            for (int i = 0; i < count - 1; i++) {
                first = first.next;
                helper = helper.next;
            }
            //此时first指向的节点即为出圈的节点
            System.out.printf("小孩%d出圈 \n", first.no);

            first = first.next;
            helper.next = first;//一直循环
        }
        System.out.printf("显示最后在圈中的小孩编号%d\n", first.no);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容

  • 链表 今天来了解下数据结构中的链表含义。 1 链表和数组的区别 数组是需要一块连续的内存空间来存储,对内存的要求比...
    雨蒙_snow阅读 1,020评论 0 1
  • 源码地址请点击此处 链表也是一种常见的数据结构,从数据结构类型上区分,链表属于存储结构的一种:链式存储结构。和顺序...
    柏丘君阅读 430评论 0 0
  • 链表基础 链表 链表是由一组不必相连【不必相连:可以连续也可以不连续】的内存结构,按特定的顺序链接在一起的抽象数据...
    ITRenj阅读 352评论 0 0
  • 链表是线性表的一种。线性表是最基本、最简单、也是最常用的一种数据结构。 线性表中数据元素之间的关系是一对一的关系,...
    骑摩托马斯阅读 645评论 0 3
  • 在上篇文章中,我们已经说过了链表除了简单的那一种单向链表外,还有其它的几种形式。当然,这也是链表这种结构的一大特点...
    ZyBlog阅读 52评论 0 0