Java数据结构-惊了腾讯居然出这种单向链表面试题

🛺前言:

        单向链表,数据结构中最基础的数据结构之一,本来我的计划是在我Java数据结构专栏的前几期就发布的,可是,理想和时间总是有冲突的,对此,最近我会把几种基础数据类型陆续推出,如:双向链表,队列,环形队列等共五篇,之后会更新史上最通俗易懂的八大排序算法,让我们拭目以待吧!

        单向链表的概述🔜单向链表的创建思路🔜单向链表的代码实现与分析🔜腾讯大厂面试题及解析🔜结论🔚

        如果喜欢勤奋的作者 ➡️ 戳我😁

往期精彩:

        迷宫回溯问题(蓝桥必备)


💞单向链表的概述: 

  单向链表:

  • 单向链表是一个有序列表
  • 单向链表是以节点的形式存储元素的
  • 节点之间的内存地址并非连续的,这是与数组这种数据结构最大的区别
  • 头节点是一个单向链表的灵魂,若头节点丢失,则整个单向链表丢失

 节点: 

  • 节点有两个属性,分别是data域和next域(data域放自己想要放的元素) (next域是存放下一个节点内存地址)
  • 头节点的data域为null,不能存储数据
  • 头节点可以有,也可以没有 

💓单向链表的创建思路:

  1.  首先创建一个节点类,里面创建对应的属性(data与next)
  2.  其次创建一个单向链表类,用于管理节点和添加方法(属性是头节点)
  3.  创建增删改插遍历的方法(详细的思路在代码分析处)

芜湖,完成了! 


❤️‍🔥单向链表的代码实现与分析: 

注意:本单向链表的创建以水浒英雄的信息的基础上进行创建 

  •  创建一个节点类

public 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;
    }

    public HeroNode() {

    }

    public String toString() {
        return "英雄好汉的编号:" + no + " 名字:" + name + " 绰号:" + nickname;
    }

}

 代码分析:

  1. 属性中的no,name,nickname是思路中的data域,用于存放数据
  2. 属性中的next对应思路中的next,用于存放下一个节点的内存地址(是has a的关系即包含关系)
  3. 需要提供无参构造有参构造.无参构造的目的是为了创建一个头节点,有参构造的目的是为了创建一般的节点
  4. 有参构造不能涉及下一个节点的创建
  • 创建一个单向链表类

HeroNode head = new HeroNode();
  1.  属性是为了创建一个头节点,从而去管理整个单向链表
  •  创建一个遍历的方法

public void print() throws LinkedListException {
        if(head.next == null) {
            throw new LinkedListException("链表为空,无法遍历");
        }
        var cur = head.next;
        while(cur != null) {
            System.out.println(cur);
            cur = cur.next;
        }
    }

  代码分析:

  1. 当我们去遍历单向链表的时候,首先要做的就是判断该单向链表是否为空,若为空则直接抛出异常
  2. 我们需要创建一个辅助引用cur去遍历链表(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
  3. 如果单向链表不为空,说明单向链表有且至少有一个节点,所以给辅助引用赋予第一个节点的内存地址
  4. 如果辅助引用指向的节点,不为空,即可以经行遍历输出,输出完后记得要跳到下一个节点.

        图解:

 

  •   创建一个尾部增加节点的方法

public void finalAdd(HeroNode node) {
        //头节点的临时引用
        var cur = head;
        while (cur.next != null) {
            //跳转下一个节点
            cur = cur.next;
        }
        cur.next = node;
    }

代码分析:

  1. 因为我们不知道该单向链表是否为空,所以我们不能想输出方法那样给辅助引用cur赋予第一个节点的内存地址,否则可能会出现空指针异常的情况
  2. 当一个节点的next域是null的时候,说明该节点是尾节点
  3. 在尾节点的next赋予下一个节点的内存地址相当于增加节点

 

  •   创建一个插入节点的方法

public void InsertedAddByNo(HeroNode node) throws LinkedListException {
        boolean flag = false;
        var cur = head;
        while(true) {
            if(cur.next == null) {
                break;
            }
            if(cur.next.no > node.no){
                break;
            }else if(cur.next.no == node.no) {
                flag = true;
                break;
            }
            cur = cur.next;
        }
        if(!flag) {
            node.next = cur.next;
            cur.next = node;
            return;
        }
        throw new LinkedListException("该编号已存在,无法添加");
    }

 代码分析:

  1.  注意的是,该插入的逻辑是按no这个属性的升序插入
  2.  一般插入节点删除节点都要打一布尔标记,以判断是否能插入或者能删除
  3.  本代码同理得,由于不知道该单向链表是否有节点,所以辅助引用应当指向头节点
  4.  第一个if的含义:如果是尾节点,直接跳出循环,避免空指针异常,也说明了直接将节点放入最后
  5.  第二个if的含义:如果该节点的下一个节点的no大于待插入节点的no,说明该节点介于这两者之间

图解 

说明:

        单向链表中删除节点和插入节点都是要找该节点的上一个节点 

     6. 若该节点的下一个节点的no与node的no相等,说明该节点已经有了,应该改flag,避免插入

     7.最后通过flag,判断该节点是否已经存在,再加入(赋值顺序很重要)

     8.首先要node.next = cur.next;让他们两个同时指向一个节点,然后cur.next = node;让一个断掉

图解

  •  创建一个删除节点的方法

public void delete(int no) throws LinkedListException {
        boolean flag = false;
        var cur = head;
        while(true) {
            if(cur.next == null) {
                break;
            }
            if(cur.next.no == no) {
                flag = true;
                break;
            }
            cur = cur.next;
        }
        if(flag) {
            cur.next = cur.next.next;
            return;
        }
        throw new LinkedListException("没有此节点");
    }

代码分析:

  1. 删除节点需要打一布尔标记,默认匹配不到待删除的节点
  2. 本代码同理得,由于不知道该单向链表是否有节点,所以辅助引用应当指向头节点
  3. 循环第一个if是判断是否为空,若为空直接跳出
  4. 循环第二个if是判断下一个节点是否为待删除的节点,如果是改变flag,立刻退出,否则跳到下一个节点
  5. 最后通过flag来删除节点

 图解:

 

  •   创建一个修改节点的方法

public void update(HeroNode newNode) throws LinkedListException {
        boolean flag = false;
        var cur = head.next;
        while(true) {
            if(cur == null) {
                break;
            }
            if(cur.no == newNode.no) {
                flag = true;
                break;
            }
            cur = temp.next;
        }
        if(flag) {
            cur.name = newNode.name;
            cur.nickname = newNode.nickname;
            return;
        }
        throw new LinkedListException("没有此节点");
    }

 代码分析:

  1. 需要打一布尔标记,默认该节点不存在
  2. 若修改节点,说明链表中肯定有节点,若读者不放心,想增强代码健壮性,可以选择加入一个判断语句if(head.next == null) 若成立则抛异常
  3. 循环中的第一个if的目的是用于判断链表是否遍历完,与遍历方法的一样,此条件若成立,说明没有符合条件的节点被更改
  4. 循环中的第二个if的目的是用于匹配.匹配成功就退出循环并改标记
  5. 最后根据标记修改即可(该方法较为简单,不详细阐述)

 完整代码:

package datastructure.chapter01.linked_list.single_linked_list.instance;

// 注意: 代码中的tmep = cur
public class SingleLinkedList {

    HeroNode head = new HeroNode();

    public HeroNode getHead() {
        return head;
    }

    public void finalAdd(HeroNode node) {
        //头节点的临时引用
        var temp = head;
        while (true) {
            if(temp.next == null) {
                break;
            }
            //跳转下一个节点
            temp = temp.next;
        }
        temp.next = node;
    }

    public void print() throws LinkedListException {
        if(head.next == null) {
            throw new LinkedListException("链表为空,无法遍历");
        }
        var temp = head.next;
        while(true) {
            if(temp == null){
                break;
            }
            System.out.println(temp);
            temp = temp.next;
        }
    }

    public void InsertedAddByNo(HeroNode node) throws LinkedListException {
        boolean flag = false;
        var temp = head;
        while(true) {
            if(temp.next == null) {
                break;
            }
            if(temp.next.no > node.no){
                break;
            }else if(temp.next.no == node.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if(!flag) {
            node.next = temp.next;
            temp.next = node;
            return;
        }
        throw new LinkedListException("该编号已存在,无法添加");
    }

    public void update(HeroNode newNode) throws LinkedListException {
        boolean flag = false;
        var temp = head.next;
        while(true) {
            if(temp == null) {
                break;
            }
            if(temp.no == newNode.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if(flag) {
            temp.name = newNode.name;
            temp.nickname = newNode.nickname;
            return;
        }
        throw new LinkedListException("没有此节点");
    }

    public void delete(int no) throws LinkedListException {
        boolean flag = false;
        var temp = head;
        while(true) {
            if(temp.next == null) {
                break;
            }
            if(temp.next.no == no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if(flag) {
            temp.next = temp.next.next;
            return;
        }
        throw new LinkedListException("没有此节点");
    }
}

面试题:!!!

 查找链表的倒数第k个节点:

    /**
     *
     * @param headNode 头节点
     * @return 有效节点数
     *
     */
    public static int size(HeroNode headNode) {
        if(headNode.next == null) {
            return 0;
        }
        int size = 0;
        for(var cur = headNode.next;cur != null;size++) {
            cur = cur.next;
        }
        return size;
    }
/**
     *
     * @param heroNode 头节点
     * @param lastIndex 倒数第lastIndex个节点
     * @return 返回该节点
     * @throws LinkedListException lastIndex不合理异常
     */
public static HeroNode findLast(HeroNode heroNode,int lastIndex) throws LinkedListException{
        if(heroNode.next == null) {
            return null;
        }
        int size = size(heroNode);
        if(lastIndex <= 0 || lastIndex > size){
            throw new LinkedListException("lastIndex不合理");
        }
        int length = size - lastIndex;
        var cur = heroNode.next;
        for (int i = 0; i < length ; i++) {
            cur = cur.next;
        }
        return cur;
    }

代码分析:

  1. 第一个方法是size()方法,用于计算总节点数,由于和上述的遍历太像,第二个方法中的参数已说明,相信小伙伴都能看懂,博主就不展开阐述了
  2. 我们首先要判断链表是否为空,如果是空则没有倒数之说
  3. 下一个if的作用是校验传入lastIndex的合理性,如果倒数的数都比总数大或者倒数的数是负数,明显不合理
  4. 将总数减去倒数的数值就是到目标节点所需要的次数(如 10 - 1 = 9 而 (1代表倒数))
  5. 通过循环找到并返回即可

  单向链表的反转(腾讯):

public static void reverse(HeroNode head) {
        if(head.next == null || head.next.next == null) {
            return;
        }
        var cur = head.next;
        HeroNode next = null;
        var reverseNode = new HeroNode();
        while(cur != null) {
            //这段代码很考究,放在最后会导致空指针异常
            next = cur.next;
            cur.next = reverseNode.next;
            reverseNode.next = cur;
            cur = next;
        }
        head.next = reverseNode.next;
    }

 代码分析:

  • 我们开始逆转单向链表时,我要要先判断单向链表是否为空,或者单向链表是否只有一个元素,如果是,直接结束方法(一个元素或者没有元素无法逆转)
  • 我们要建立两个引用,一个指向当前被取出的节点,一个指向下一个节点目的是:为了不丢失链表​​​​​​​

原因:如果我们只创建一个引用去指向当前被取出的节点,那么当节点被取出后,无法找到原来的下一个节点,即整个链表就丢失了 

  • 创建一个一个辅助的头节点,用于交换
  • 第二个引用需要放入循环中赋值,因为当我们的cur被取走了之后,那么next就是cur,next此刻需要被重新赋值,所以next需要动态获取,若在循环外获取是静态获取

图解: 

  •  通过图解可以得到,循环中的中间两条语句可以完美的嫁接在辅助节点中,即头插法
  •  不断通过头插法,可以将最前面的节点放在最后,将最后面的节点放在最前面
  •  然后将原来的两个引用进行移动即可
  •  最后,将辅助头节点下面的所有节点交还给头节点,逆序完成

  ​​​​​​​逆序输出节点:

 public static void reversePrint(HeroNode head) {
        if(head.next == null) {
            return;
        }
        var cur = head.next;
        var stack = new Stack<HeroNode>();
        while(cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        while(stack.size() > 0) {
            System.out.println(stack.pop());
        }
    }

  代码分析:

  1. 毫无新奇的前四行(校验是否为空,赋值)
  2. 此处用到了栈的知识(先入后出,后入先出),根据这个原理,就可以将他逆序

 问题

用前面的那个先逆序在输出行不行? 可以,但是代码效率低


结论:

        本篇博客主要对单向链表的增删插改遍历进行了讲述,此外,还对面试题:逆序输出,链表逆序,倒数节点等问题一一概述,特别的是,我们最需要掌握的是一下几点:

        删除,插入,反转链表三个大点

        🚇下一站:shell排序和快速排序(排序我会两两推出,共四篇)

  • 15
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爪哇土著、JOElib

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值