链表

162 阅读8分钟

链表

链表 (Linked List) 介绍

链表有序的列表,但是它在内存中是存储如下

image.png

  • 小结上图:
    • 链表是以节点的方式来存储,是链式存储
    • 每个节点包含 data 域, next 域:指向下一个节点.
    • 如图:发现链表的各个节点不一定是连续存储.
    • 链表分带头节点的链表没有头节点的链表,根据实际的需求来确定
  • 单链表(带头结点) 逻辑结构示意图如下

image.png

单链表的应用实例

 使用带head头的单向链表实现 –水浒英雄排行榜管理 ,完成对英雄人物的增删改查操作

第一种方法在添加英雄时,直接添加到链表的尾部

 思路分析示意图

image.png

第二种方式在添加英雄时,根据排名将英雄插入到指定位置 (如果有这个排名,则添加失败,并给出提示)

思路分析示意图

image.png image.png

修改节点功能

  • 思路
    • 先找到该节点,通过遍历,
    • temp.name = newHeroNode.name ; temp.nickname=newHeroNode.nickname

删除节点

  • 思路分析的示意图:

image.png

 代码

package com.www.linked;

import java.util.Stack;

/**
 * 单向链表
 * <p>
 *
 * @author Www
 * <p>
 * 邮箱: 483223**@qq.com
 * <p>
 * 创建时间: 2022/7/29  11:35  星期五
 * <p>
 */
public class LinkedListTest {
    /**
     * 方法入口
     *
     * @param args 参数
     */
    public static void main(String[] args) {
        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero5 = new HeroNode(5, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
        
        // 创建要给数据的链表
        SingleLinkedList singleLinkedList = new SingleLinkedList();
        // 加入数据
        singleLinkedList.addByOrder(hero5);
        singleLinkedList.addByOrder(hero1);
        singleLinkedList.addByOrder(hero3);
        singleLinkedList.addByOrder(hero2);
        singleLinkedList.addByOrder(hero3);
        singleLinkedList.addByOrder(hero4);
        // 获取头节点
        HeroNode head = singleLinkedList.getHead();
        System.out.println(head);
        // 展示链表数据
        singleLinkedList.list();
        // 修改
        boolean update = singleLinkedList.update(new HeroNode(5, "有用", "至多某"));
        System.out.println("修改节点 " + update);
        System.out.println("\n===============  修改后的链表  =================\n");
        // 展示链表数据
        singleLinkedList.list();
        // 删除
    /*    boolean delete = singleLinkedList.delete(5);
        boolean delete3 = singleLinkedList.delete(3);
        boolean delete2 = singleLinkedList.delete(2);
        boolean delete4 = singleLinkedList.delete(4);
        boolean delete1 = singleLinkedList.delete(1);
        System.out.println("删除节点  = " + delete);
        System.out.println("删除节点  = " + delete3);
        System.out.println("删除节点  = " + delete2);
        System.out.println("删除节点  = " + delete4);
        System.out.println("删除节点  = " + delete1);*/
        System.out.println("\n===============  删除后的链表   =================\n");
        // 展示链表数据
        singleLinkedList.list();
        
        // 获取具体的节点
        System.out.println("singleLinkedList.getHeroNodeByNo(5) = " + singleLinkedList.getHeroNodeByNo(5));
        // 获取有效节点的个数(不包括头节点)
        Integer effectLength = singleLinkedList.getEffectLength(singleLinkedList.getHead());
        System.out.println("effectLength = " + effectLength);
        System.out.println("singleLinkedList.findLastIndexNode(singleLinkedList.getHead(),2) = " + singleLinkedList.findLastIndexNode(singleLinkedList.getHead(), effectLength));
        
        // 反转
        singleLinkedList.reverseList(singleLinkedList.getHead());
        System.out.println("反转后的链表  ");
        // 展示链表数据
        singleLinkedList.list();
        
        // 反转链表 ,不改变原链表
        System.out.println("  反转链表 ,不改变原链表  ");
        singleLinkedList.reversePrint(singleLinkedList.getHead());
        
    }
}

/**
 * 定义 SingleLinkedList 管理 英雄
 */
class SingleLinkedList {
    /**
     * 先初始化一个头节点, 头节点不动 , 不存放具体的数据
     */
    private final HeroNode head = new HeroNode(0, "", "");
    
    /**
     * 返回头节点
     *
     * @return 头节点
     */
    public HeroNode getHead() {
        
        return head;
    }
    
    /**
     * 添加节点到单向链表
     * <p>
     * 不考虑编号
     * <p>
     * 找到当前链表的最后一个节点
     * <p>
     * 将最后一个节点的 next 指向新的节点
     *
     * @param heroNode 待添加的节点
     */
    public void add(HeroNode heroNode) {
        // 头节点不能动 ,需要一个辅助遍历 tmp
        HeroNode temp = getHead();
        // 判断要添加的元素是否已经存在
        boolean flag = false;
        // 遍历 链表 ,找到最后一个节点
        while (temp.getNext() != null) {
            if (temp.getNext().getNo() == heroNode.getNo()) {
                flag = true;
                break;
            }
            // 没有找到 将 temp 后移
            temp = temp.getNext();
        }
        if (flag) {
            System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.getNo());
        } else {
            temp.setNext(heroNode);
        }
    }
    
    /**
     * 添加并排序
     *
     * @param heroNode 待添加的节点
     */
    public void addByOrder(HeroNode heroNode) {
        // 头节点不能动 ,需要一个辅助遍历 tmp
        // 因为是单链表, 且我们找的是位于添加位置的前一个节点,否则插入不了
        HeroNode temp = getHead();
        // flag 标志添加的编号是否已存在,默认为 false
        boolean flag = false;
        while (temp.getNext() != null) {
            if (temp.getNext().getNo() > heroNode.getNo()) {
                break;
            } else if (temp.getNext().getNo() == heroNode.getNo()) {
                // 说明希望添加的 heroNode 的编号已经存在
                flag = true;
                break;
            }
            // 后移, 遍历当前的链表
            temp = temp.getNext();
        }
        if (flag) {
            System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.getNo());
        } else {
            // 插入到链表中 , temp的后面
            heroNode.setNext(temp.getNext());
            temp.setNext(heroNode);
        }
    }
    
    /**
     * 遍历链表
     */
    public void list() {
        // 判断链表是否为空
        if (head.getNext() == null) {
            System.out.println("链表为空!!!");
            return;
        }
        HeroNode temp = head.getNext();
        while (temp != null) {
            // 输出节点信息
            System.out.println(temp);
            // 将 temp 节点信息后移
            temp = temp.getNext();
        }
        
        
    }
    
    /**
     * 修改
     *
     * @param heroNode
     */
    public boolean update(HeroNode heroNode) {
        // 判断链表是否为空
        if (head.getNext() == null) {
            System.out.println("链表为空 !!!");
        }
        // 找到修改的节点 ,根据 no 编号
        // 定义一个辅助变量
        HeroNode temp = head.getNext();
        boolean flag = false;
        while (temp != null) {
            if (temp.getNo() == heroNode.getNo()) {
                flag = true;
                break;
            }
            // temp 向后移 ,实现遍历
            temp = temp.getNext();
        }
        if (flag) {
            // 找到了要修改的数据
            temp.setName(heroNode.getName());
            temp.setNickName(heroNode.getNickName());
            return true;
        } else {
            // 没找到
            System.out.printf("没有找到 编号 %d 的节点,不能修改\n", heroNode.getNo());
            return false;
        }
    }
    
    /**
     * 根据 No 删除节点
     *
     * @param no 待删除节点的 No
     * @return true 删除成功 ; false 删除失败
     */
    public boolean delete(Integer no) {
        HeroNode temp = head;
        boolean flag = false;
        while (temp.getNext() != null) {
            if (temp.getNext().getNo() == no) {
                // 找到了要删除节点 的前一个节点 temp
                flag = true;
                break;
            }
            temp = temp.getNext();
        }
        if (flag) {
            // 删除
            temp.setNext(temp.getNext().getNext());
            return true;
        } else {
            System.out.printf("要删除的 %d 节点不存在\n", no);
            return false;
        }
    }
    
    /**
     * 根据 No 获取英雄的详细信息
     *
     * @param no No
     * @return HeroNode 对象
     */
    public HeroNode getHeroNodeByNo(Integer no) {
        HeroNode temp = head.getNext();
        if (temp == null) {
            System.out.println("链表为空 !!!");
        }
        boolean flag = false;
        while (temp != null) {
            if (temp.getNo() == no) {
                flag = true;
                break;
            }
            temp = temp.getNext();
        }
        if (flag) {
            // 找到了
            return new HeroNode(no, temp.getName(), temp.getNickName());
        } else {
            return null;
        }
    }
    
    /**
     * 获取到单链表的节点的个数 (如果是带头节点的链表,需求不统计头节点)
     *
     * @param headNode 链表的头节点
     * @return 返回有效节点的个数
     */
    public Integer getEffectLength(HeroNode headNode) {
        if (headNode == null) {
            return 0;
        }
        HeroNode temp = headNode.getNext();
        if (temp == null) {
            // 空链表
            return 0;
        }
        int length = 0;
        while (temp != null) {
            length++;
            temp = temp.getNext();
        }
        return length;
    }
    
    /**
     * 查找单链表中的倒数第k个结点 【新浪面试题】
     * <p>
     * 思路
     * <p>
     * 1. 编写一个方法,接收head节点,同时接收一个index
     * <p>
     * 2. index 表示是倒数第index个节点
     * <p>
     * 3. 先把链表从头到尾遍历,得到链表的总的长度 getLength
     * <p>
     * 4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到
     * <p>
     * 5. 如果找到了,则返回该节点,否则返回nulll
     *
     * @param headNode
     * @param index
     * @return
     */
    public HeroNode findLastIndexNode(HeroNode headNode, Integer index) {
        if (headNode == null) {
            return null;
            
        }
        // 定义辅助变量
        HeroNode temp = headNode.getNext();
        if (temp == null) {
            // 空链表
            return null;
        }
        // 第一次遍历得到链表的有效长度
        Integer effectLength = getEffectLength(headNode);
        // 第二次遍历 size -index 位置就是我们倒数的第 k 个节点
        if (index < 0 || index > effectLength) {
            return null;
        }
        for (int i = 0; i < effectLength - index; i++) {
            temp = temp.getNext();
        }
        return temp;
    }
    
    /**
     * 将单链表反转
     *
     * @param heroNode 链表的头节点
     */
    public void reverseList(HeroNode heroNode) {
        if (heroNode == null) {
            return;
        }
        //定义一个辅助的指针(变量) ,帮助我们遍历原来的链表
        HeroNode cur = heroNode.getNext();
        if (cur == null || cur.getNext() == null) {
            // 如果当前链表为空, 或 只有一个节点,则无需反转,直接返回
            return;
        }
        // 指向当前节点 【cur】 的下一个节点
        HeroNode next;
        HeroNode reverseHead = new HeroNode(0, "", "");
        while (cur != null) {
            //先暂时保存当前节点的下一个节点,因为后面需要使用
            next = cur.getNext();
            //将cur的下一个节点指向新的链表的最前端
            cur.setNext(reverseHead.getNext());
            // 将 cur 链接到新的链表上
            reverseHead.setNext(cur);
            // 让 cur 后移
            cur = next;
        }
        heroNode.setNext(reverseHead.getNext());
    }
    
    
    /**
     * 可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
     * <p>
     * 不改变原链表结构
     */
    public void reversePrint(HeroNode heroNode) {
        if (heroNode == null) {
            // 空链表 , 不打印
            return;
        }
        // 创建一个站栈 ,将各个节点压入栈
        Stack<HeroNode> stack = new Stack<>();
        HeroNode temp = heroNode.getNext();
        while (temp != null) {
            // 将链表的所有节点压入栈
            stack.push(temp);
            // temp 后移,
            temp = temp.getNext();
        }
        // 将栈中的节点进行打印 , pop 出栈
        while (stack.size() > 0) {
            // 栈的特点时 先进后出
            System.out.println(stack.pop());
        }
    }
    
}

/**
 * 英雄类
 */
class HeroNode {
    /**
     * 英雄编号
     */
    private int no;
    /**
     * 英雄姓名
     */
    private String name;
    /**
     * 英雄称号
     */
    private String nickName;
    /**
     * 指向下一个节点【英雄】
     */
    private HeroNode next;
    
    public HeroNode() {
    }
    
    public HeroNode(int no, String name, String nickName, HeroNode next) {
        this.no = no;
        this.name = name;
        this.nickName = nickName;
        this.next = next;
    }
    
    public HeroNode(int no, String name, String nickName) {
        this.no = no;
        this.name = name;
        this.nickName = nickName;
    }
    
    public int getNo() {
        return no;
    }
    
    public void setNo(int no) {
        this.no = no;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getNickName() {
        return nickName;
    }
    
    public void setNickName(String nickName) {
        this.nickName = nickName;
    }
    
    public HeroNode getNext() {
        return next;
    }
    
    public void setNext(HeroNode next) {
        this.next = next;
    }
    
    @Override
    public String toString() {
        return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + ", nickName='" + nickName + '\'' +
                //                ", next=" + next +
                '}';
    }
}

单链表面试题(新浪、百度、腾讯)

 单链表的常见面试题有如下:

1)求单链表中有效节点的个数

    /**
     * 获取到单链表的节点的个数 (如果是带头节点的链表,需求不统计头节点)
     *
     * @param headNode 链表的头节点
     * @return 返回有效节点的个数
     */
    public Integer getEffectLength(HeroNode headNode) {
        if (headNode == null) {
            return 0;
        }
        HeroNode temp = headNode.getNext();
        if (temp == null) {
            // 空链表
            return 0;
        }
        int length = 0;
        while (temp != null) {
            length++;
            temp = temp.getNext();
        }
        return length;
    }

2)查找单链表中的倒数第k个结点 【新浪面试题】

    /**
     * 查找单链表中的倒数第k个结点 【新浪面试题】
     * <p>
     * 思路
     * <p>
     * 1. 编写一个方法,接收head节点,同时接收一个index
     * <p>
     * 2. index 表示是倒数第index个节点
     * <p>
     * 3. 先把链表从头到尾遍历,得到链表的总的长度 getLength
     * <p>
     * 4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到
     * <p>
     * 5. 如果找到了,则返回该节点,否则返回nulll
     *
     * @param headNode
     * @param index
     * @return
     */
    public HeroNode findLastIndexNode(HeroNode headNode, Integer index) {
        if (headNode == null) {
            return null;
        }
        // 定义辅助变量
        HeroNode temp = headNode.getNext();
        if (temp == null) {
            // 空链表
            return null;
        }
        // 第一次遍历得到链表的有效长度
        Integer effectLength = getEffectLength(headNode);
        // 第二次遍历 size -index 位置就是我们倒数的第 k 个节点
        if (index < 0 || index > effectLength) {
            return null;
        }
        for (int i = 0; i < effectLength - index; i++) {
            temp = temp.getNext();
        }
        return temp;
    }

3)单链表的反转【腾讯面试题,有点难度】

 思路分析图解

image.png

image.png

 
    /**
     * 将单链表反转
     *
     * @param heroNode 链表的头节点
     */
    public void reverseList(HeroNode heroNode) {
        if (heroNode == null) {
            return;
        }
        //定义一个辅助的指针(变量) ,帮助我们遍历原来的链表
        HeroNode cur = heroNode.getNext();
        if (cur == null || cur.getNext() == null) {
            // 如果当前链表为空, 或 只有一个节点,则无需反转,直接返回
            return;
        }
        // 指向当前节点 【cur】 的下一个节点
        HeroNode next;
        HeroNode reverseHead = new HeroNode(0, "", "");
        while (cur != null) {
            //先暂时保存当前节点的下一个节点,因为后面需要使用
            next = cur.getNext();
            //将cur的下一个节点指向新的链表的最前端
            cur.setNext(reverseHead.getNext());
            // 将 cur 链接到新的链表上
            reverseHead.setNext(cur);
            // 让 cur 后移
            cur = next;
        }
        heroNode.setNext(reverseHead.getNext());
    }

4)从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】

image.png

 
    /**
     * 可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
     * <p>
     * 不改变原链表结构
     */
    public void reversePrint(HeroNode heroNode) {
        if (heroNode == null) {
            // 空链表 , 不打印
            return;
        }
        // 创建一个站栈 ,将各个节点压入栈
        Stack<HeroNode> stack = new Stack<>();
        HeroNode temp = heroNode.getNext();
        while (temp != null) {
            // 将链表的所有节点压入栈
            stack.push(temp);
            // temp 后移,
            temp = temp.getNext();
        }
        // 将栈中的节点进行打印 , pop 出栈
        while (stack.size() > 0) {
            // 栈的特点时 先进后出
            System.out.println(stack.pop());
        }
    }

5)合并两个有序的单链表,合并之后的链表依然有序