搞懂数据结构-链表(更新版)

搞懂数据结构-链表

1.背景

动态数组的申请数组大小有个明显的缺点,可能会造成内存空间的大量浪费。那能不能需要用多少就申请多少内存大小呢?链表可以做到这一点。

2.单向链表

2.1单向链表的设计

在这里插入图片描述
代码:

public class LinkedList<E> {
    //存储元素的大小
    private int size;
    //指向第一个元素的节点
    private Node<E> first;

    //因为节点类只在本类中使用,所以定义为内部类
    private static class Node<E>{
        E element;
        Node<E> next;
        //构造方法
        public Node(E element, Node<E> next) {
            this.element = element;
            this.next = next;
        }
    }
}
2.1.1设计思路

在这里插入图片描述

链表的接口设计和动态数组大同小异,所以在这里抽取一个公共接口出来。如果不写接口,而是写父类,让继承的话,就会存在方法的差异。所以更好的方法是,定义一个接口,然后再定义个抽象类,抽象类中放公共方法,而子类中不同的方法重写接口中的就行。没有必要在父类中写不是公共方法的代码。

抽象类存在的好处,如果子类直接去实现接口,那么接口里面的方法全部都得实现,而抽象类不需要

理解:

  • 先抽象出一个抽象类,里面存放公共方法,然后再向上再抽象出一个接口,存放所有方法,包括不公共的方法
  • 或者先抽象出接口,然后子类都去实现接口,发现子类有公共代码,将公共代码抽取出来成抽象成一个类B。让这个类B去实现接口,而子类去实现这个类B。但是只要是实现了接口就必须重写接口中所有方法,而类B的功能只是存放公共方法,所有这时候将类B变成抽象类就可以解决这个问题

抽象类不对外公开,只提供公共代码的作用。所有在使用过程中一般

List<Integer> list = new LinkedList<>();
2.1.2接口设计
public interface List<E> {

    public static final  int ELEMENT_NOT_FIND = -1;
    //元素大小
    int size();
    //判断是否为空
    boolean isEmpty();
    //添加元素到指定位置
    void add(int index,E element);
    //添加元素的末尾
    void add(E element);
    //删除指定元素
    E remove(int index);
    //清空元素
    void clear();
    //修改元素
    E set(int index, E element);
    //判断是否含有
    boolean contain(E element);
    //查找元素
    E get(int index);

}
2.1.3抽象类

类似于动态数组设计中一样,我们将一些通用的方法写在一个抽象类中,那么新类继承抽象类就可以直接使用了,并且让抽象类实现接口,并实现一些通用方法

public abstract class AbstractList<E> implements List<E> {
    protected int size;
    // 下标越界抛出的异常
    protected void outOfBounds(int index) {
        throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
    }
    // 检查下标越界(不可访问或删除size位置)
    protected void rangeCheck(int index){
        if(index < 0 || index >= size){
            outOfBounds(index);
        }
    }
    // 检查add()的下标越界(可以在size位置添加元素)
    protected void rangeCheckForAdd(int index) {
        if (index < 0 || index > size) {
            outOfBounds(index);
        }
    }

    @Override
    public boolean contain(E element) {
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }

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

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void add(E element) {
        add(size,element);
    }
}

2.2单向链表的增删改查

public class SingledList<E> extends AbstractList<E> {
    
    private Node<E> first;
    
    private static class Node<E>{
        E element;
        Node<E> next;
        public Node(E element, Node<E> next) {
            this.element = element;
            this.next = next;
        }
    }
}
//找到索引对应的值
@Override
public E get(int index) {
    return node(index).element;
}

//找到索引对应的node
public Node<E> node(int index){
    rangeCheck(index);
    Node node = first;
    for (int i = 0; i < index; i++){
        node = node.next;
    }
    return node;
}

@Override
public int indexOf(E element) {
    if (element == null){
        Node<E> node = first;
        for (int i = 0; i < size; i++){
            if (node.element == null) return i;
            node = node.next;
        }
    }else {
        Node<E> node = first;
        for (int i = 0; i < size; i++){
            if (node.element.equals(element)) return i;
            node = node.next;
        }
    }
    return ELEMENT_NOT_FOUND;
}
//修改
@Override
public E set(int index, E element) {
    //查找节点,node做了索引边界检测,所以这里不需要再做
    Node<E> node = node(index);
    E old = node.element;
    //修改element
    node.element = element;
    return old;
}
//添加元素
@Override
public void add(int index, E element) {
    rangeCheckForAdd(index);
    if (index == 0){
        //first指向新Node,新Node原来的first指向的node,也就是first
        first = new Node<>(element,first);
    }else {
        //需要找到前面的元素
        Node<E> pre = node(index - 1);
        //pre指向新Node,新Node指向pre.next
        pre.next = new Node<>(element,pre.next);
    }
    size++;
}
//删除
@Override
public E remove(int index) {
    rangeCheck(index);
    Node<E> node = first;
    if (index == 0){
        first = first.next;
    }else {
        //要删除节点的前一个
        Node<E> pre = node(index - 1);
        //要删除的节点
        node = pre.next;
        //前一个节点指向要删除的后一个节点
        pre.next = node.next;

    }
    size--;
    //返回被删除节点的元素值
    return node.element;
}

清空元素

在这里插入图片描述

只需要将size设置为0,fisrt指向null,后面的 Node没有被指向,在 Java 中会自动被垃圾回收。

@Override
public void clear() {
    size = 0;
    first = null;
}
完整代码
public class SingledList<E> extends AbstractList<E> {

    private Node<E> first;

    private static class Node<E>{
        E element;
        Node<E> next;
        public Node(E element, Node<E> next) {
            this.element = element;
            this.next = next;
        }
    }
    //添加元素
    @Override
    public void add(int index, E element) {
        if (index == 0){
            //first指向新Node,新Node原来的first指向的node,也就是first
            first = new Node<>(element,first);
        }else {
            //需要找到前面的元素
            Node<E> pre = node(index - 1);
            //pre指向新Node,新Node指向pre.next
            pre = new Node<>(element,pre.next);
        }
        size++;
    }

    //删除
    @Override
    public E remove(int index) {
        Node<E> node = first;
        if (index == 0){
            first = first.next;
        }else {
            //要删除节点的前一个
            Node<E> pre = node(index - 1);
            //要删除的节点
            node = pre.next;
            //前一个节点指向要删除的后一个节点
            pre.next = node.next;

        }
        size--;
        //返回被删除节点的元素值
        return node.element;
    }

    @Override
    public void clear() {
        size = 0;
        first = null;
    }

    //修改
    @Override
    public E set(int index, E element) {
        Node<E> node = node(index);
        E old = node.element;
        node.element = element;
        return old;
    }

    //找到索引对应的值
    @Override
    public E get(int index) {
        return node(index).element;
    }

    //找到索引对应的node
    public Node<E> node(int index){
        rangeCheck(index);
        Node node = first;
        for (int i = 0; i < index; i++){
            node = node.next;
        }
        return node;
    }
        @Override
    public int indexOf(E element) {
        if (element == null){
            Node<E> node = first;
            for (int i = 0; i < size; i++){
                if (node.element == null) return i;
                node = node.next;
            }
        }else {
            Node<E> node = first;
            for (int i = 0; i < size; i++){
                if (node.element.equals(element)) return i;
                node = node.next;
            }
        }
        return ELEMENT_NOT_FOUND;
    }
}
小结:

可以注意到,增和删在头节点这里的处理和其他位置的处理略有不同。因为头节点只是一个指针,并不是一个节点,没有next属性。

2.3 虚拟头节点

为了统一增和删的逻辑操作,增加一个虚拟头节点来实现。(解题的时候可以添加虚拟头节点这个来简化代码)

在这里插入图片描述

发生变化的地方

1.构造方法

//构造初始虚拟节点
public SingledList() {
    this.first = new Node<E>(null, null);
}

2.查找节点方法

//找到索引对应的node
public Node<E> node(int index){
    rangeCheck(index);
    //以前是first,现在是first.next
    //因为现在first是虚拟节点,里面有next属性
    Node node = first.next;
    for (int i = 0; i < index; i++){
        node = node.next;
    }
    return node;
}

3.增方法

//添加元素
@Override
public void add(int index, E element) {
    rangeCheckForAdd(index);
    //需要找到前面的元素,如果index为0,index-1会出现负数,所以需要判断
    Node<E> pre = index == 0 ? first:node(index - 1);
    //pre指向新Node,新Node指向pre.next
    pre = new Node<>(element,pre.next);
    size++;
}

4.删方法

//删除
@Override
public E remove(int index) {
    rangeCheck(index);
    //要删除节点的前一个
    Node<E> pre = index == 0 ? first:node(index - 1);
    //要删除的节点
    Node<E> node = pre.next;
    //前一个节点指向要删除的后一个节点
    pre.next = node.next;
    size--;
    //返回被删除节点的元素值
    return node.element;
}

3.双向链表

3.1双向链表的设计

在单向链表中,要访问尾节点,每次都要从头节点开始。为了提升性能,双向链表可以进行优化。

双向链表的接口和抽象都和单向链表一样,只是在增删改查中不一样。

在这里插入图片描述
代码

public class DoubleLinkedList<E> extends AbstractList<E> {
    //头尾节点
    private Node<E> first;
    private Node<E> last;

    private static class Node<E>{
        E element;
        Node<E> prev;
        Node<E> next;

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

3.2双向链表的增删改查

//根据索引获取元素
@Override
public E get(int index) {
    return node(index).element;
}

//根据索引找到节点
public Node<E> node(int index){
    rangeCheck(index);
    if (index < (size >> 1)){
        Node<E> node = first;
        for (int i = 0; i < index ; i++){
            node = node.next;
        }
        return node;
    }else {
        Node<E> node = last;
        for (int i = size -1; i > index ; i++){
            node = node.next;
        }
        return node;
    }
}
//修改元素
@Override
public E set(int index, E element) {
    E old = node(index).element;
    node(index).element = element;
    return old;
}
/**
 * 添加元素:
 * 尾节点添加,特殊情况是添加第一个
 * 中间添加,特殊情况是头节点
 * @param index
 * @param element
 */
@Override
public void add(int index, E element) {
    rangeCheckForAdd(index);
    if (index == size){
        Node<E> oldLast = last;
        last = new Node<>(element,oldLast,null);
        if (oldLast == null){
            //第一个节点,头尾指针指向同一个
            first = last;
        }else {
            oldLast.next = last;
        }
    }else {
        Node<E> next = node(index);
        Node<E> prev = next.prev;
        Node<E> node = new Node<>(element,prev,next);
        next.prev = node;
        //index = 0,头节点添加不一样
        if (prev == null){
            first = node;
        }else {
            prev.next = node;
        }
    }
    size++;
}
//清空链表
@Override
public void clear() {
    size = 0;
    first = null;
    last = null;
}
//删除元素
@Override
public E remove(int index) {
    rangeCheck(index);
    Node<E> node = node(index);
    Node<E> prev = node.prev;
    Node<E> next = node.next;
    //头部
    if (prev == null){
        first = next;
    }else {
        prev.next = next;
    }
    //尾部
    if (next == null){
        last = prev;
    }else {
        next.prev = prev;
    }
    size--;
    return node.element;
}

4.单向环形链表

在这里插入图片描述

和单向链表对比,操作不同的地方在于增和删除的头部处理

//添加元素
@Override
public void add(int index, E element) {
    rangeCheckForAdd(index);
    if (index == 0){
        Node<E> newFirst = new Node<>(element, first);
        // 拿到最后一个节点, 上面先不要直接改first, 否则下面找节点会出现问题
        Node<E> last = (size == 0) ? newFirst : node(size - 1);
        last.next = newFirst;
        first = newFirst;
    }else {
        //需要找到前面的元素
        Node<E> pre = node(index - 1);
        //pre指向新Node,新Node指向pre.next
        pre.next = new Node<>(element,pre.next);
    }
    size++;
}

//删除
@Override
public E remove(int index) {
    rangeCheck(index);
    Node<E> node = first;
    if (index == 0){
    	if (size == 1){
            first = null;
    	}else{
    		Node<E> last = node(size - 1);
			first = first.next;
			last.next = first;
    	}
    }else {
        //要删除节点的前一个
        Node<E> pre = node(index - 1);
        //要删除的节点
        node = pre.next;
        //前一个节点指向要删除的后一个节点
        pre.next = node.next;
    }
    size--;
    //返回被删除节点的元素值
    return node.element;
}

5.双向循环链表

在这里插入图片描述
和双向向链表对比,操作不同的地方在于增和删除的头部操作

@Override
public void add(int index, E element) {
    rangeCheckForAdd(index);
    if (index == size){
        Node<E> oldLast = last;
        last = new Node<>(element,oldLast,first);
        if (oldLast == null){
            //第一个节点,头尾指针指向同一个
            first = last;
            first.next = first;
			first.prev = first;
        }else {
            oldLast.next = last;
            first.prev = last;
        }
    }else {
        Node<E> next = node(index);
        Node<E> prev = next.prev;
        Node<E> node = new Node<>(element,prev,next);
        next.prev = node;
        prev.next = node;
        //index = 0,头节点添加不一样
        if (next == first){
            first = node;
        }
    }
    size++;
}

//删除元素
@Override
public E remove(Node<E> node) {
    if (size == 1) {
        first = null;
        last = null;
    } else {
        Node<E> pre = node.pre;
        Node<E> next = node.next;
        pre.next = next;
        next.pre = pre;

        if (node == first) { // index == 0
            first = next;
        }

        if (node == last) { // index == size - 1
            last = pre;
        }
    }
    size--;
    return node.element;
}

6.静态链表

  • 可以通过数组来模拟链表,称为静态链表
  • 数组的每个元素存放 2 个数据:值、下个元素的索引
  • 数组 0 位置存放的是头结点信息

如果一个元素只能存放1个数据

  • 那就使用 2 个数组,1 个数组存放索引关系,1 个数组存放值

练习

1.删除链表中的节点

请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。

分析:我们之前的思路是找到要删除节点的前一个节点,但是太麻烦,可以采取用节点覆盖的方法

注意:这里没有尾节点,有尾节点这个方法就不能用了

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public void deleteNode(ListNode node) {
        //用被删除的节点的下一个节点进行覆盖
        node.val = node.next.val;
        node.node = node.next.next;
    }
}

2.删除链表中等于给定值 val 的所有节点。

分析:注意删除的头结点是不一样的。所以加了一个虚头节点

代码:

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //新加一个虚头节点,让头节点和其他节点的操作统一起来
        ListNode header = new ListNode(-1);
        header.next = head;
        ListNode cur = header;
        while (cur.next != null){
            if (cur.next.val == val){
                cur.next = cur.next.next;
            }else{
                cur = cur.next;
            }
        }
        return header.next;
    }
}

3.删除排序链表中重复元素

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

分析:根据有序,相邻判断是否相等就行

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode header = new ListNode(10000);
        header.next = head;
        ListNode cur = header;
        while (cur.next != null){
            if (cur.val == cur.next.val){
                cur.next = cur.next.next;
            }else{
                cur = cur.next;
            }
        }
        return header.next;
    }
}

4.反转一个链表

反转一个单链表。

例子:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NUL
递归方法

在这里插入图片描述

假设方法实现了,那么调用方法会变成怎么样呢(这个思考很关键,利用递归,必须得先知道递归的函数的作用)
在这里插入图片描述
调用下一个,这里执行成功后,开始归的代码
在这里插入图片描述
代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        //边界条件
        if (head==null || head.next==null) return head;
        // 这个是递
        ListNode newHead = reverseList(head.next);
        // 拿到4,也就是head.next
        // 让4,指向下一个节点就是head,这里是归
        //下面的归操作,就是newHead-1-2-3-4,已经得到了
        //然后连接5得到newHead-1-2-3-4-5的过程
        head.next.next = head;
        // 5的next应该指向空
        head.next = null;
        return newHead;
    }
}

递归总结:

  1. 理解递归函数的作用
  2. 写出递归中的递:也就是调用下一个
  3. 写出递归中的归:也就是调用下一个成功后,怎么操作让上一个成立
  4. 写出边界条件,避免死循环。
头插法
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        // 头插法
        ListNode newHead = null;
        while (head != null){
            //保存下一个节点,防止找不到
            ListNode temp = head.next;
			//插入
            head.next = newHead;
            newHead = head;
			//原来的head指向下一个节点
            head = temp;
        }
        return newHead;
    }
}

5 .判断链表是否有环

快慢指针解决

代码

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) return false;
        ListNode slow = head;
        ListNode fast = head.next;
        //fast为空的话,fast.next就会空指针,所有需要两个都不为空
        while(fast != null && fast.next != null){
            if (slow.val == fast.val) return true;
            slow = slow.next;
            fast = fast.next.next;
        }
        return false;
    }
}

6.循环链表解决约瑟夫问题

public static void josephus(){
    //循环链表
	CircleLinkedList<Integer> list = new CircleLinkedList<>();
	for(int i = 1; i <= 8; i++){
		list.add(i);
	}
    //指向头节点
	list.reset(); // current->1
	while(!list.isEmpty()){
        //这里定义走的步数
		list.next();
		list.next();
        //删除节点
		System.out.println(list.remove());
	}
}

注:仅用于学习交流

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值