02 链表表


1 链表介绍

1.1 简单概述

除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。由这两部分信息组成一个"结点"(如概述旁的图所示),表示线性表中一个数据元素。线性表的链式存储表示,有一个缺点就是要找一个数,必须要从头开始找起,十分麻烦。

  • 链表是以节点的方式来存储的
  • 内存上来看:链表存储空间不连续(不像数组)
  • 逻辑上来看:链表属于线性结构

下面想一下链表的基本操作:

  1. 取数据
  2. 插入数据
  3. 删除数据
  4. 获取数据量

1.2 接口定义

根据这些定义一个接口

package study.wyy.struct.list;

/**
 * @author by wyaoyao
 * @Description
 * @Date 2021/1/10 11:06 上午
 */
public interface List<T> {

    /****
     * 添加数据
     * @param t
     */
    void add(T t);

    /****
     * 指定位置添加数据
     * @param index
     * @param t
     */
    void add(int index, T t);

    /****
     * 返回链表中数据个数
     * @return
     */
    int size();

    /****
     * 删除索引处数据
     * @param index
     */
    void remove(int index);

    /****
     * 删除指定数据
     * @param t
     */
    void remove(T t);

    /*****
     * 修改数据
     * @param index
     * @param t
     */
    void set(int index,T t);
 
 	/****
     * 获取指定索引位置的数据指定数据
     * @param index
     */
    T get(int index);


}


2 单链表

在这里插入图片描述
如何判断是最后一个节点:当前节点的next为null,没有指向下一个节点,就表示当前节点为最后一个节点

2.1 思路分析

添加数据分析
  • 插入到最后
    1. 找到最后一个节点:next为null
    2. 将next指向当前插入的节点
  • 指定位置插入
    1. 找到要插入的位置的前一个节点
    2. 前一个节点的next指向当前插入的节点
    3. 当前插入的节点指向

在这里插入图片描述

删除数据分析

我们提供了两种删除:一个根据索引位置删除,一个是根据数据删除,根据数据删除,相比根据索引位置删除,只需要找到数据所在索引位置即可,再根据索引位置删除:

  1. 找到索引所在的位置的前一个节点
  2. 将前一个节点的next指向要删除节点的的下一个节点
  3. 并将删除节点的next指向null

删除第一个节点,上面的逻辑就不能复用了,针对这情况,链表在设计的时候,大家通常会增加一个头节点(head),这个节点不存数据,它的next指向的是我们的第一个节点,这样删除第一个时候,就和上面逻辑可以保持一致

在这里插入图片描述

2.2 代码实现

节点定义

根据上面的分析:节点→两部分组成

  • 数据:data
  • 指向下一个节点的引用:next
final class Node<T>{
    T data;
    Node<T> next;

    public Node(T data) {
        this(data,null);
    }
    public Node(T data,Node<T> next) {
        this.data = data;
        this.next = next;
    }
}
实现接口
package study.wyy.struct.list;


/**
 * @author by wyaoyao
 * @Description
 * @Date 2021/1/10 3:51 下午
 */
public class MyLinkedList<T> implements List<T> {
    // 记录链表中的数据个数
    private int size = 0;

    // 定义一个头结点
    private Node<T> head = new Node<T>();

    /*@Override
    public void add(T t) {
        // 尾部添加
        // 0 构造节点
        Node<T> newNode = new Node<>(t);
        // 1 遍历找到最后一个节点:最后一个节点的判断标准是next为null
        // 这里必须定义一个临时变量(引用)指向头结点,从头结点开始变量,头结点是不能动的,动了就改变链表的顺序了
        Node<T> p = this.head;
        while (true){
            if(p.next == null){
                // 找到了最后一个节点,此时p已经指向了最后一个节点
                break;
            }
            // 没有找到,就进行后移
            p = p.next;
        }
        // 2 此时p已经指向了最后一个节点
        // 最后一个节点的next指向我们的新节点
        p.next = newNode;
        // size ++
        size++;

    }*/

    @Override
    public void add(T t) {
       /*
       // 改进,size-1的位置就是最后一个节点
        // 0 构造新的节点
        Node<T> newNode = new Node<>(t);
        // 这里必须定义一个临时变量(引用)指向头结点,从头结点开始变量,头结点是不能动的,动了就改变链表的顺序了
        Node<T> p = this.head;
        // size-1的位置就是最后一个节点
        for(int i = 0; i<size(); i++){
            // 进行后移
            p = p.next;
        }
        // 2 此时p已经指向了最后一个节点
        // 最后一个节点的next指向我们的新节点
        p.next = newNode;
        // size ++
        size++;
        */
        // 在分析:上面的逻辑,完全和指定位置添加是一样的了
        this.add(this.size(), t);

    }

    @Override
    public void add(int index, T t) {
        // 指定位置添加
        // 0 构造新的节点
        Node<T> newNode = new Node<>(t);
        Node<T> p = this.head;
        // 1 找到这个位置的前一个节点,比如 index = 2, 就要从head节点移动2(index)次,移动到index=1的位置
        for (int i = 0; i < index; i++) {
            // 进行后移
            p = p.next;
        }
        // 此时p已经指到,前一个节点
        // 新节点的next指向后一个节点(此时后一个节点就是p.next)
        newNode.next = p.next;
        // 再将前一个节点next指向新的节点
        p.next = newNode;
        this.size++;
    }

    @Override
    public int size() {
        return this.size;
    }

    @Override
    public void remove(int index) {
        // 校验一下index不能超出下界
        if (index >= this.size() || index < -1) {
            throw new IndexOutOfBoundsException("数组越界");
        }
        Node<T> p = this.head;
        // 1 找到这个位置的前一个节点,// 比如index=2,就要移动index=1的节点 p->index=0->index=1,也就是移动2(index)次
        for (int i = 0; i < index; i++) {
            // 进行后移
            p = p.next;
        }
        // 此时p已经指到前一个节点
        // 将前一个节点的next指向要删除节点的的下一个节点 : p.next要删除节点,  p.next.next 要删除节点的的下一个节点
        Node<T> removeNode = p.next;
        p.next = removeNode.next;
        // 并将删除节点的next指向null
        removeNode.next = null;
        this.size--;
    }

    @Override
    public void remove(T t) {
        // 1 遍历找到要删除数据的索引位置
        Node<T> p = this.head;
        int index = 0;
        // 用于标记是否找到要删除的数据
        boolean flag = false;
        for (int i = 0; i < this.size(); i++) {
            Node<T> next = p.next;
            if (t.equals(next.data)) {
                index = i;
                flag = true;
                break;
            }
            // 后移p
            p = p.next;
        }
        if(flag){
            // 说明没有找到数据
            // 2 删除指定索引位置的数据
            remove(index);
        }
    }

    @Override
    public void set(int index, T t) {
        // 校验一下index不能超出下界
        if (index >= this.size() || index < -1) {
            throw new IndexOutOfBoundsException("数组越界");
        }
        // 定义一个临时指针p,
        Node<T> p = this.head;
        // 移动p指向指定索引位置
        // 比如index=2,就要移动index=2的节点 p->index=0->index=1->index=2,也就是移动3(index+1)次
        for (int i = 0; i < index + 1; i++) {
            p = p.next;
        }
        p.data = t;

    }

    @Override
    public T get(int index) {
        // 校验一下index不能超出下界
        if (index >= this.size() || index < -1) {
            throw new IndexOutOfBoundsException("数组越界");
        }
        // 定义一个临时指针p,
        Node<T> p = this.head;
        // 移动p指向指定索引位置的前一个(此时的p.next就是当前索引位置的节点)
        // 比如index=2,就要移动index=1的节点 p->index=0->index=1,也就是移动2(index)次
        for (int i = 0; i < index; i++) {
            p = p.next;
        }
        return p.next.data;
    }


    public void reverse() {
        // 0 定义一个新的头节点
        Node<T> reverseHead = new Node<T>();
        // 1 定义一个临时指针,指向第一个元素(在移动p的时候,p指向的的就是当前节点)
        Node<T> p = this.head.next;
        // 2 记录当前节点的下一个节点,为了能继续寻找节点
        Node<T> next = null;
        // 2 移动p进行遍历: 不是p.next不等于null,p.next==null,此时p还在最后一个节点,还需要后移,才不会漏掉最后一个节点
        while (p != null){
            // 记录当前节点的下一个节点,为了能继续寻找节点
            next = p.next;
            // 当前节点要指向前一个取下的节点,比如当前是第二个节点,这个节点的next要指向原先的第一个节点
            p.next = reverseHead.next;
            // reverseHead指向当前节点
            reverseHead.next = p;
            // p后移
            p =next;
        }
        // 将head.next指向reverseHead的next
        head.next = reverseHead.next;

    }


    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder("[");
        Node<T> p = this.head;
        for (int i = 0; i < this.size(); i++) {
            Node<T> next = p.next;
            stringBuilder.append(next.data.toString());
            if (i != size() - 1) {
                stringBuilder.append(",");
            }
            // 后移p
            p = p.next;
        }
        stringBuilder.append("]");
        return stringBuilder.toString();

    }

    final class Node<T> {
        T data;
        Node<T> next;

        public Node() {
        }

        public Node(T data) {
            this(data, null);
        }

        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}

测试:

package study.wyy.struct.list;

import sun.jvm.hotspot.utilities.Assert;

import javax.sound.midi.Soundbank;

/**
 * @author by wyaoyao
 * @Description
 * @Date 2021/1/10 6:04 下午
 */
public class Test {

    @org.junit.Test
    public void testAdd(){
        List<Integer> list = new MyLinkedList<>();
        list.add(1);
        list.add(3);
        System.out.println(list);
        list.add(1,2);
        System.out.println(list);
        list.add(4);
        list.add(6);
        System.out.println(list);
        list.add(4,5);
        System.out.println(list);
    }


    @org.junit.Test
    public void tesGet(){
        List<Integer> list = new MyLinkedList<>();
        list.add(0);
        list.add(1);
        list.add(2);
        Assert.that(0==list.get(0),"数据不对");
        Assert.that(1==list.get(1),"数据不对");
        Assert.that(2==list.get(2),"数据不对");
    }

    @org.junit.Test(expected=IndexOutOfBoundsException.class)
    public void tesSet(){
        List<Integer> list = new MyLinkedList<>();
        list.add(0);
        list.add(1);
        list.add(2);
        Assert.that(0==list.get(0),"数据不对");
        Assert.that(1==list.get(1),"数据不对");
        Assert.that(2==list.get(2),"数据不对");
        list.set(2,3);
        Assert.that(3==list.get(2),"数据不对");
        list.set(0,1);
        Assert.that(1==list.get(0),"数据不对");
        list.set(1,2);
        Assert.that(2==list.get(1),"数据不对");
        // 数组越界
        list.set(3,4);
    }

    @org.junit.Test
    public void tesRemove(){
        List<String> list = new MyLinkedList<>();
        list.add("0");
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        list.remove(0);
        // [1,2,3,4,5]
        System.out.println(list);
        list.remove(2);
        // [1,2,4,5]
        System.out.println(list);
        list.remove("4");
        //[1,2,5]
        System.out.println(list);

    }
}

2.3 扩展

2.3.1 单链表的反转
思路分析

在这里插入图片描述

大概的思路:

  1. 定义一个新的头节点
  2. 定义一个临时指针p,指向第一个元素(在移动p的时候,p指向的的就是当前节点)
  3. 移动p进行遍历: 结束条件不是p.next不等于null,p.next==null,此时p还在最后一个节点,还需要后移,才不会漏掉最后一个节点
    • 遍历时要记录当前节点的下一个节点,为了能继续寻找节点
  4. 当前取下的节点要指向前一个取下的节点,比如当前是第二个节点,这个节点的next要指向原先的第一个节点
  5. reverseHead指向当前取下的节点
  6. p后移
  7. 将head.next指向reverseHead的next
public void reverse() {
        // 0 定义一个新的头节点
        Node<T> reverseHead = new Node<T>();
        // 1 定义一个临时指针,指向第一个元素(在移动p的时候,p指向的的就是当前节点)
        Node<T> p = this.head.next;
        // 2 记录当前节点的下一个节点,为了能继续寻找节点
        Node<T> next = null;
        // 2 移动p进行遍历: 不是p.next不等于null,p.next==null,此时p还在最后一个节点,还需要后移,才不会漏掉最后一个节点
        while (p != null){
            // 记录当前节点的下一个节点,为了能继续寻找节点
            next = p.next;
            // 当前节点要指向前一个取下的节点,比如当前是第二个节点,这个节点的next要指向原先的第一个节点
            p.next = reverseHead.next;
            // reverseHead指向当前节点
            reverseHead.next = p;
            // p后移
            p =next;
        }
        // 将head.next指向reverseHead的next
        head.next = reverseHead.next;

    }

测试

@org.junit.Test
public void test(){
    MyLinkedList<String> list = new MyLinkedList<>();
    list.add("0");
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");
    System.out.println(list);
    list.reverse();
    System.out.println(list);
}

3 双向链表

在这里插入图片描述
单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除 时节点,总是找到 temp,temp 是待删除节点的前一个节点

单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。

双向链表的节点分为三部分

  1. next: 指向下一个节点
  2. pre: 指向后一个节点
  3. data: 数据域

3.1 思路分析

添加数据分析

在这里插入图片描述

添加到最后:

  1. 找到最后一个节点(next=null)
  2. 新插入节点的pre指向最后一个节点
  3. 最后一个节点的next指向新查询的节点
    同样还是需要一个临时指针从头节点开始遍历寻找

指定位置添加

  1. 找到要插入位置的前一个节点
  2. 新节点的next指向后一个节点(newNode.next = p.next)
  3. 再将前一个节点next指向新的节点(p.next=newNode)
  4. 后一节点的pre指向当前的新节点(newNode.next.pre = newNode)
  5. 新节点的pre指向前一个节点 (newNode.pre = p)
删除数据分析

在这里插入图片描述

  1. 找到要删除的节点p
  2. p的前一个节点next指向p的后一个节点(p.pre.next = p.next)
  3. p的后一个节点的pre指向p的前一个节点(p.next.pre = p.pre)
    注意:删除的就是最后一个节点呢(p.next =null),上面p.next.pre就会抛出空指针异常,这个情况是要特殊处理的
    此时就没有后一个节点
  4. p.next=null,p.pre=null

3.3 代码实现

package study.wyy.struct.list;

/**
 * 双向链表
 *
 * @author by wyaoyao
 * @Description
 * @Date 2021/2/27 5:16 下午
 */
public class MyDoubleLinkedList<T> implements List<T> {
    // 记录链表中的数据个数
    private int size = 0;
    // 定义一个头结点
    private Node<T> head = new Node<>();

    @Override
    public void add(T t) {
        // 构造新的节点
        Node<T> newNode = new Node<>(t);
        // 定义一个临时指针,指向头结点,进行遍历
        Node p = this.head;
        while (true) {
            if (p.next == null) {
                // 找到了最后一个节点,此时p已经指向了最后一个节点
                break;
            }
            // 后移p
            p = p.next;
        }
        // 此时p已经指向了最后一个节点,最后一个节点的next指向我们的新节点
        p.next = newNode;
        // 新节点的pre指向p(也就是前一个节点)
        newNode.pre = p;
        this.size++;
    }

    @Override
    public void add(int index, T t) {
        // 0 构造新的节点
        Node<T> newNode = new Node<>(t);
        // 定义一个临时指针,指向头结点,进行遍历
        Node p = this.head;
        // 找到要插入位置的前一个节点
        // 比如 index = 2, 就要从head节点移动2(index)次,移动到index=1的位置
        for (int i = 0; i < index; i++) {
            // 移动p
            p = p.next;
        }
        // 此时p已经指到,前一个节点
        // 新节点的next指向后一个节点(此时后一个节点就是p.next)
        newNode.next = p.next;
        // 再将前一个节点next指向新的节点
        p.next = newNode;
        // 后一节点的pre指向当前的新节点
        newNode.next.pre = newNode;
        // 新节点的pre指向前一个节点 (注意:p的赋值必须放到最后,p中间要是发生变化,就会导致节点顺序不对)
        newNode.pre = p;
        this.size++;
    }

    @Override
    public int size() {
        return this.size;
    }

    @Override
    public void remove(int index) {
        // 找到要删除的节点p
        Node<T> p = getNode(index);
        // p的前一个节点next指向p的后一个节点(p.pre.next = p.next)
        p.pre.next = p.next;
        // p的后一个节点的pre指向p的前一个节点(p.next.pre = p.pre)
        // 注意:删除的就是最后一个节点呢(p.next =null),上面p.next.pre就会抛出空指针异常,这个情况是要特殊处理的
        // 此时就没有后一个节点
        if(null != p.next){
            p.next.pre = p.pre;
        }
        this.size--;
    }

    @Override
    public void remove(T t) {
        // 1 遍历找到要删除数据的索引位置
        Node<T> p = this.head;
        int index = 0;
        // 用于标记是否找到要删除的数据
        boolean flag = false;
        for (int i = 0; i < this.size(); i++) {
            Node<T> next = p.next;
            if (t.equals(next.data)) {
                // 找到了
                index = i;
                flag = true;
                break;
            }
            // 后移p
            p = p.next;
        }
        if(flag){
            // 说明没有找到数据
            // 2 删除指定索引位置的数据
            remove(index);
        }
    }

    @Override
    public void set(int index, T t) {
        getNode(index).data = t;
    }

    @Override
    public T get(int index) {
        return getNode(index).data;
    }

    private Node<T> getNode(int index) {
        // 校验一下index不能超出下界
        if (index >= this.size() || index < -1) {
            throw new IndexOutOfBoundsException("数组越界");
        }
        // 定义一个临时指针,指向头结点,进行遍历
        Node<T> p = this.head;
        // 移动p指向指定索引位置的前一个(此时的p.next就是当前索引位置的节点)
        // 比如index=2,就要移动index=1的节点 p->index=0->index=1,也就是移动2(index)次
        for (int i = 0; i < index; i++) {
            p = p.next;
        }
        // 取出数据
        return p.next;
    }


    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder("[");
        Node<T> p = this.head;
        for (int i = 0; i < this.size(); i++) {
            Node<T> next = p.next;
            stringBuilder.append(next.data.toString());
            if (i != size() - 1) {
                stringBuilder.append(",");
            }
            // 后移p
            p = p.next;
        }
        stringBuilder.append("]");
        return stringBuilder.toString();
    }

    final class Node<T> {
        T data;
        Node<T> next;
        Node<T> pre;

        public Node() {
        }

        public Node(T data) {
            this(data, null, null);
        }

        public Node(T data, Node<T> next, Node<T> pre) {
            this.data = data;
            this.next = next;
            this.pre = pre;
        }
    }
}

测试:

package study.wyy.struct.list;

import sun.jvm.hotspot.utilities.Assert;


/**
 * @author by wyaoyao
 * @Description
 * @Date 2021/1/10 6:04 下午
 */
public class DoubleLinkedListTest {

    @org.junit.Test
    public void testAdd(){
        List<Integer> list = new MyDoubleLinkedList<>();
        list.add(1);
        list.add(3);
        // [1,3]
        System.out.println(list);
        list.add(1,2);
        // [1,2,3]
        System.out.println(list);
        list.add(4);
        list.add(6);
        // [1,2,3,4,6]
        System.out.println(list);
        // [1,2,3,4,5,6]
        list.add(4,5);
        System.out.println(list);
    }


    @org.junit.Test
    public void tesGet(){
        List<Integer> list = new MyDoubleLinkedList<>();
        list.add(0);
        list.add(1);
        list.add(2);
        Assert.that(0==list.get(0),"数据不对");
        Assert.that(1==list.get(1),"数据不对");
        Assert.that(2==list.get(2),"数据不对");
    }

    @org.junit.Test(expected=IndexOutOfBoundsException.class)
    public void tesSet(){
        List<Integer> list = new MyDoubleLinkedList<>();
        list.add(0);
        list.add(1);
        list.add(2);
        Assert.that(0==list.get(0),"数据不对");
        Assert.that(1==list.get(1),"数据不对");
        Assert.that(2==list.get(2),"数据不对");
        list.set(2,3);
        Assert.that(3==list.get(2),"数据不对");
        list.set(0,1);
        Assert.that(1==list.get(0),"数据不对");
        list.set(1,2);
        Assert.that(2==list.get(1),"数据不对");
        // 数组越界
        list.set(3,4);
    }

    @org.junit.Test
    public void tesRemove(){
        List<String> list = new MyDoubleLinkedList<>();
        list.add("0");
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        list.remove(0);
        // [1,2,3,4,5]
        System.out.println(list);
        list.remove(2);
        // [1,2,4,5]
        System.out.println(list);
        list.remove("4");
        //[1,2,5]
        System.out.println(list);

    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值