数据结构和算法学习第二节 链表

单链表

单链表概述

    基本介绍
    链表示有序的,但是存储的地址不一定示连续的,存储示意如图下:
在这里插入图片描述
    简要说明,head头指针指向的是 data域a1,a1的next域指向下一个存储的节点,链表存储的数据并不一定是连续的。
    1、链表是以节点的方式存储的
    2、每个节点包含data域(存储数据)、next域指向的下一个存储的节点
    3、链表的各个节点不一定是连续存放的
    4、链表可以有头节点,也可以不包含头节点
    逻辑结构示意图如下
在这里插入图片描述

单链表实现

    实现一个单向链表->完成单链表的增加、删除、修改、查找
在这里插入图片描述
    代码实现

package com.example.data.sparse.linked;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Objects;
import java.util.UUID;

/**
 * @author zjt
 * 单链表实现
 */
public class MineSingleLinkedList<E> {

    // 头节点 不存放具体数组就是表示单链表的头 也不要动 就表示一个链表的头
    Node<E> head = new Node<>(null, null);

    // 新增一个节点到单链表
    // 1、只在链表的尾部增加
    // 2、将最后一个节点的next 指向新的节点
    public void add(E e) {
        // 需要一个临时变量 temp 用于遍历链表,找到最后一个节点
        Node<E> temp = this.head;
        // 遍历链表 找到链表的尾节点
        while (temp.next != null) {
            // 没有找到指针后移
            temp = temp.next;
        }
        temp.next = new Node<>(e);
    }

    // 指定下标插入
    public void add(int index, E e) {
        // 头部节点不动  依旧通过辅助变量temp来帮助找到需要添加的位置
        // 因为是单链表 所以找到的temp是位于 添加位置的钱一个节点 否则添加会出现问题
        Node<E> temp = this.head;
        for (int i = 0; i < index; i++) {
            if ((temp = temp.next) == null) {
                throw new IndexOutOfBoundsException(String.format("Index:%d, Size:%d", index, i));
            }
        }
        // 插入到链表中
        temp.next = new Node<>(e, temp.next);
    }

    // 单链表的修改
    // 这里简单实现根据下标修改
    public void update(int index, E e) {
        if (head.next == null) {
            throw new RuntimeException("链表为空");
        }
        Node<E> temp = head.next;
        for (int i = 0; i < index; i++) {
            if ((temp = temp.next) == null) {
                throw new IndexOutOfBoundsException(String.format("Index:%d, Size:%d", index, i));
            }
        }
        temp.item = e;
    }

    // 删除节点
    // 1、找到待删除节点的前一个节点
    // 2、临时变量 e == temp.next.item
    // 3、被删除的节点 将不会有其它引用指向,会被垃圾回收机制回收
    public void remove(E e) {
        if (head.next == null) {
            throw new RuntimeException("链表为空");
        }
        Node<E> temp = head;
        while (temp.next != null) {
            // 找到待删除节点的前一个节点
            if (e.equals(temp.next.item)) {
                temp.next = temp.next.next;
            } else {
                temp = temp.next;
            }
        }
    }

    // 显示链表
    public void list() {
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }
        // 头节点不要动 需要一个辅助变量遍历
        Node<E> temp = head.next;
        while (temp != null) {
            // 输出节点的信息
            System.out.println(temp);
            // 将temp后移
            temp = temp.next;
        }
    }

    private static class Node<E> { // 静态内部类用于处处数据节点

        E item; // 存储的数据

        Node<E> next; // 下一个节点的引用

        // 构造器
        protected Node(E item, Node<E> next) {
            this.item = item;
            this.next = next;
        }

        public Node(E item) {
            this.item = item;
            this.next = null;
        }

        @Override  // 显示方便
        public String toString() {
            return "Node{" +
                    "item=" + item +
                    '}';
        }
    }

}

@Getter
@Setter
@ToString
//@Data 不使用这个 @Data会重写很多我们不需要的方法 导致调试出现意外
class User {

    private Integer id;

    private String name;

    private String mobile;

    public User(Integer id, String name, String mobile) {
        this.id = id;
        this.name = name;
        this.mobile = mobile;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (!Objects.equals(id, user.id)) return false;
        if (!Objects.equals(name, user.name)) return false;
        return Objects.equals(mobile, user.mobile);
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (mobile != null ? mobile.hashCode() : 0);
        return result;
    }
    
}

class Client {

    public static void main(String[] args) {
        MineSingleLinkedList<User> singleLinkedList = new MineSingleLinkedList<>();
        System.out.println("实现尾节点新增");
        for (int i = 0; i < 4; i++) {
            if (i == 2) {
                continue;
            }
            User user = new User(i, UUID.randomUUID().toString(), "1339890999" + i);
            singleLinkedList.add(user);
        }
        singleLinkedList.list();
        // 插入位置3元素
        System.out.println("实现插入节点新增");
        User user = new User(2, UUID.randomUUID().toString(), "1111");
        singleLinkedList.add(2, user);
        singleLinkedList.list();
        System.out.println("实现指定下标修改");
        singleLinkedList.update(0, user);
        singleLinkedList.list();
        System.out.println("实现equals删除所有");
        singleLinkedList.remove(user);
        singleLinkedList.list();
    }
    
}

     为了使单链表更好用,可以添加更多的额外属性,比如尾节点 tail,链表长度size等属性。

单链表案例

    案例一
    在上述代码中已经实现的单链表,获取单链表的节点的个数,(如果是头节点仅指向下一个节点,没有存放数据,需要不统计头节点)

 // 获取单链表的节点的个数
    public int getListSize() {
        // 这个例子头节点没有存储数据 不能统计这个头节点
        Node<E> temp = head.next;
        if (temp == null) {
            return 0;
        }
        int size = 0;
        while (temp != null) {
            size++;
            temp = temp.next;
        }
        return size;
    }

    案例二
    获取单链表的倒数第n个元素

    // 获取单链表的倒数第n个节点
    // 思路一
    // 1、方法接收一个index 就是倒数n个节点
    // 2、先把链表遍历一遍获取链表的长度size
    // 3、获取到size后,从链表的第一个开始遍历 size - index个就是链表的倒数第n个节点
    // 4、存在节点返回 不存在返回null
    public E getLastIndexNode1(int index) {
        // 获取链表的长度
        assert index > 1;
        int size = this.getListSize();
        if (head.next == null || size < index) {
            return null;
        }
        int nodeIndex = size - index;
        Node<E> temp = head.next;
        for (int i = 0; i < nodeIndex; i++) {
            temp = temp.next;
        }
        return temp.item;
    }

    // 获取单链表的倒数第n个节点
    // 思路二
    // 1、方法接收一个index
    // 2、声明两个临时变量,其中一个遍历index次 第二个变量开始遍历,
    // 3、遍历完成第二个临时变量就是倒数第index个元素
    public E getLastIndexNode2(int index) {
        if (head.next == null) {
            return null;
        }
        assert index > 1; // index 须大于0
        Node<E> temp = head.next;
        Node<E> temp1 = head.next;
        // 分成两个循环写 方便理解
        // 1、先将变量 temp 后移index 次
        for (int i = 0; i < index; i++) {
            if (temp == null) {
                return null;
            } else {
                temp = temp.next;
            }
        }
        // 2、两个变量一起后移 temp后移 直至没有元素
        while (temp != null) {
            temp = temp.next;
            temp1 = temp1.next;
        }
        return temp1.item;
    }

    案例三
    单链表的反转

    // 单链表的反转
    // 思路1 新建链表 头插法实现
    public void reverseList() {
        if (head.next == null || head.next.next == null) {
            // 当前链表为空 或者只有一个节点无需反转
            return;
        }
        // 临时变量
        Node<E> temp = head.next;
        // 清空链表
        head = new Node<>(null);
        while (null != temp) {
            addHeadNode(temp.item);
            temp = temp.next;
        }
    }

    // 链表头插法 每次都新增在链表的第一个位置
    public void addHeadNode(E e) {
        Node<E> temp = head.next;
        head.next = new Node<>(e, temp);
    }

    // 单链表反转
    // 思路二 类似于头插法
    public void reverseList1() {
        if (head.next == null || head.next.next == null) {
            // 当前链表为空 或者只有一个节点无需反转
            return;
        }
        Node<E> current = head.next; // 当前节点 辅助指针 用于遍历链表
        Node<E> next; // 指向当前节点[current]的下一个节点
        Node<E> reverseNode = new Node<>(null); // 设置一个反转的头节点
        // 遍历原来的链表 每遍历一个节点 就将其取出并放在新链表的 reverseNode 的最前端
        while (current != null) {
            next = current.next; // 暂时保存当前节点的下一个节点
            current.next = reverseNode.next; // 将current的下一个节点 指向新的链表的最前端
            reverseNode.next = current; // 将 反转的头节点下一个节点 指向 当前节点 current 连接到新的链表上
            current = next; // 指针后移
        }
        head.next = reverseNode.next;
    }

    案例4
    逆序打印单链表

    // 逆序打印单链表
    // 思路一:根据上面实现的单链表反转方法,先反转再打印。缺点:破坏了原有的链表结构
    // 思路二:利用递归的方式打印 缺点 因为这里头节点仅作为引用 并不实际存放数据 所以打印会将头节点的null 打印出来
    public static <E> void printfListReverseByRecursive(Node<E> head) {
        if (head.next != null) {
            printfListReverseByRecursive(head.next);
        }
        System.out.println(head.item);
    }

    // 思路三 使用栈的特性逆序打印
    public void printfListReverseByStack() {
        // 空链表 不打印
        if (head.next == null) {
            return;
        }
        // 遍历入栈
        Stack<E> stack = new Stack<>();
        Node<E> temp = head.next;
        while (temp != null) {
            stack.push(temp.item);
            temp = temp.next;
        }
        // 出栈
        while (!stack.isEmpty()) {
            System.out.println(stack.pop());
        }
    }

单向链表解决约瑟夫问题

    约瑟夫问题,假设编号1,2,3 …n个人,约定号码k(1<=k<=n)的数字,从k的位置开始数,数到m的人出列,下一个人从1开始数,依此类推直到所有人出列,产生一个出队编号。
    假定 n =5 有5个人,k=1 第一个位置开始,m=2 每数到2的人出列。
在这里插入图片描述
    出圈顺序是 2 -> 4 -> 1 -> 5 -> 3
    构建一个环形单向链表思路
    1、先创建第一个节点,让head指向该节点,并形成环形
    2、后面每当我们新增一个节点,就把该节点加入到已有的环形链表中。
    遍历环形链表
    1、借助辅助遍历temp 指向head节点,然后通过while循环遍历环形链表,temp == head 结束。
    约瑟夫问题代码实现

package com.example.data.sparse.linked;

/**
 * @author zjt
 * 约瑟夫问题
 */
public class Joseph {

    public static void main(String[] args) {
        RingLinkedList<Integer> list = new RingLinkedList<>();
        for (int i = 1; i <= 125 ; i++) {
            list.add(i);
        }
        System.out.println();
        list.getJoseph(10,20);
    }

}

class RingLinkedList<E> {

    // 头节点
    Node<E> head;

    // 尾节点
    Node<E> tail;

    // 链表的节点个数
    int size = 0;

    // 使用尾插法新增数据
    // 形成一个环状,让尾节点的next 指向头节点这样就形成一个环状
    public void add(E e) {
        // 当前的尾节点
        Node<E> t = this.tail;
        Node<E> newNode = new Node<>(e, null);
        // 将新节点作为尾节点
        tail = newNode;
        if (t == null) { // 新增时链表是空的
            // 赋值头节点
            head = newNode;
        } else {
            // 不是空链表
            // 将之前的尾节点的next 指向新的节点
            t.next = newNode;
        }
        size++;
        // 让尾节点的next 指向头节点
        tail.next = head;
    }

    // 遍历当前循环链表
    public void show() {
        Node<E> temp = this.head;
        if (temp == null) {
            System.out.println("空链表");
            return;
        }
        while (true) {
            System.out.println(temp.toString());
            if (temp.next == head) {
                break;
            }
            temp = temp.next;

        }
    }

    // 约瑟夫环出队问题  1 <= K <= this.size 表示从第几个开始 m 表示数几次出圈
    public void getJoseph(int k, int m) {
        if (head == null || k < 0 || k > size) {
            return;
        }
        // 创建临时变量 辅助约出圈问题
        Node<E> helper = head; // 指向头节点
        Node<E> temp = tail; // 指向尾节点 它的下一个节点是头节点
        // 出圈报数前先根据需求后移k-1次
        // 例如后移2
        for (int i = 0; i < k - 1; i++) {
            helper = helper.next;
            temp = temp.next;
        }
        // 当元素报数时,temp 和 helper需要后移 m-1次 然后出圈
        while (true) {
            if (helper == temp) { // 说明圈中仅有一个节点
                break;
            }
            for (int i = 0; i < m - 1; i++) {
                temp = temp.next;
                helper = helper.next;
            }
            // 此时temp 指向的节点 就是需要出圈的节点
            System.out.println("出圈的节点为:" + helper.item);
            // 将temp 指向的节点出圈
            helper = helper.next;
            temp.next = helper;
        }
        System.out.println("最后留在圈中的节点:" + helper.item);

    }


    private static class Node<E> {

        E item;

        Node<E> next;

        public Node(E item, Node<E> next) {
            this.item = item;
            this.next = next;
        }

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

    
    
    
    
    
    

双向链表

概述

    基本介绍:
    1、双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
    与单向链表对比
    1、单向链表的查找方向智能从头到尾的方向查找,而双向链表可以从任意一个节点向前或者向后查找。
    2、单向链表不能自我删除,需要依靠一个辅助节点(总是需要找到辅助节点的前一个节点来删除),而双向链表可以进行自我删除。
在这里插入图片描述
    双向链表添加、修改、删除、遍历思路分析
    添加(创建) 插尾法
    1、先找到双向链表的最后一个节点
    2、temp.next = newNode
    3、newNode.prev = temp
    修改:思路与单向链表基本一致
    删除
    1、双向链表可以实现自我删除某个节点
    2、直接找到需要删除的节点 delNode
    3、delNode.prev.next = delNode.next
    4、delNode.next.prev= delNode.prev
    遍历:思路与单向链表一致,但是可以向前、向后遍历

package com.example.data.sparse.linked;

import java.util.UUID;

/**
 * @author zjt
 * 双向链表
 * 没有考虑线程安全问题
 */
public class MineBothWayLinkedList<E> {

    // 声明一个头节点 跟上面的单链表有所区别 头节点存储数据
    Node<E> head;

    // 声明一个尾节点 目的是使用起来更加方便
    Node<E> tail;

    // 这里新增时使用 ++ 删除时使用-- 没有考虑线程安全问题
    int size = 0;

    public MineBothWayLinkedList() { // 构造器
    }

    // 尾插法新增节点
    public void addTail(E e) {
        // 取出尾一个节点
        Node<E> t = tail;
        // 新建节点
        Node<E> newNode = new Node<>(t, e, null);
        // 将新节点作为尾节点
        tail = newNode;
        if (t == null) { // 新增时链表是空的
            // 赋值头节点
            head = newNode;
        } else {
            // 不是空链表
            // 将之前的尾节点的next 指向新的节点
            t.next = newNode;
        }
        size++;
    }

    // 头插法新增节点
    public void addHead(E e) {
        // 取出头节点
        Node<E> h = head;
        // 新建节点
        Node<E> newNode = new Node<>(null, e, h);
        // 新节点作为头节点
        head = newNode;
        if (h == null) { // 头节点为空 证明是空链表
            tail = newNode; // 赋值尾节点
        } else {
            h.prev = newNode; // 将之前的头节点的 前驱节点指向 新的头节点
        }
        size++;
    }

    //指定下标新增节点
    public void addByIndex(int index, E e) {
        if (index > size) {
            // 指定下标大于链表长度 抛出异常
            throw new IndexOutOfBoundsException(String.format("Size %d; index %d", size, index));
        }
        if (index == 0) { // 新增头节点
            addHead(e);
        } else if (index == size) { // 新增尾节点
            addTail(e);
        } else {
            // 找到需要新增节点 的前一个节点(前驱节点)
            Node<E> temp = this.head;
            // 链表的遍历 性能不高,这里为了省事 就直接循环遍历了 可以优化
            for (int i = 1; i < index; i++) {
                temp = temp.next;
            }
            // 新增节点的前驱节点就是 临时变量 temp 后继节点 是 temp.next
            Node<E> newNode = new Node<>(temp, e, temp.next);
            // 后继节点的 前驱节点 要变更尾 新节点
            temp.next.prev = newNode;
            // 前驱节点的 后继节点 是新节点
            temp.next = newNode;
            size++;
        }
    }

    // 删除节点
    public void remove(E e) {
        if (head == null || e == null) {// 空链表无法移除
            return;
        }
        Node<E> temp = this.head;
        while (temp != null) {
            if (e.equals(temp.item)) {
                // 准备删除该节点
                Node<E> prev = temp.prev; //前驱节点
                Node<E> next = temp.next; // 后继节点
                if (prev == null) { // 前驱节点为空 删除的节点是头节点
                    head = next; // 当前节点的后继节点作为头节点
                } else { // 前驱节点不为空
                    prev.next = next; // 前驱节点的后继节点指向 当前节点的后继节点
                    temp.prev = null; // 当前节点与前驱节点断开连接
                }

                if (next == null) { // 尾节点
                    tail = prev; // 删除的是尾节点 将当前节点的前驱节点 设置为尾节点
                } else {
                    next.prev = prev;// 后继节点的前驱节点 修改为当前节点的前驱节点
                    temp.next = null; // 当前节点与后继节点断开连接
                }
                size--;
                temp.item = null; // 移除与具体元素的引用
            }
            temp = temp.next;
        }
    }

    // 根据下标修改链表元素
    public void update(int index, E e) {
        if (null == head) { // 空链表 啥也不干
            return;
        }
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException(String.format("Size %d; index %d", size, index));
        }
        Node<E> temp = this.head;
        for (int i = 0; i < index; i++) {
            temp = temp.next;
        }
        temp.item = e;
    }

    // 显示链表
    public void show() {
        if (head == null) {
            System.out.println("链表为空");
            return;
        }
        // 需要一个辅助变量遍历
        Node<E> temp = head;
        while (temp != null) {
            // 输出节点的信息
            System.out.println(temp);
            // 将temp后移
            temp = temp.next;
        }
    }

    public int getSize() {
        return size;
    }


    private static class Node<E> { // 静态内部类用于处处数据节点

        E item; // 存储的数据

        Node<E> next; // 下一个节点的引用

        Node<E> prev;// 上一个节点的引用

        // 构造器
        protected Node(Node<E> prev, E item, Node<E> next) {
            this.prev = prev;
            this.item = item;
            this.next = next;
        }

        @Override  // 显示方便
        public String toString() {
            return "Node{" +
                    "item=" + item +
                    '}';
        }
    }

}

class Client2 {

    public static void main(String[] args) {
        MineBothWayLinkedList<User> singleLinkedList = new MineBothWayLinkedList<>();
        System.out.println("实现尾节点新增");
        for (int i = 0; i < 4; i++) {
            User user = new User(i, UUID.randomUUID().toString(), "1339890999" + i);
            singleLinkedList.addTail(user);
        }
        singleLinkedList.show();
        // 测试头节点新增
        System.out.println("测试头节点新增");
        User user = new User(4, UUID.randomUUID().toString(), "13398909994");
        singleLinkedList.addHead(user);
        singleLinkedList.show();
        // 插入新增
        System.out.println("插入新增");
        User user1 = new User(2001, UUID.randomUUID().toString(), "1339");
        singleLinkedList.addByIndex(3, user1);
        singleLinkedList.show();
        System.out.println(singleLinkedList.getSize());
        System.out.println("删除节点");
        singleLinkedList.remove(user);
        singleLinkedList.show();
        System.out.println(singleLinkedList.getSize());
        System.out.println("删除节点");
        singleLinkedList.remove(user1);
        singleLinkedList.show();
        System.out.println(singleLinkedList.getSize());
        System.out.println("修改节点");
        singleLinkedList.update(3, user1);
        singleLinkedList.show();
        System.out.println(singleLinkedList.getSize());
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值