数据结构与算法(三)--- 线性表之链式存储结构

本文深入解析链表的概念,包括单链表、双链表及其应用,如MessageQueue和麻将排序。通过实例说明链表操作,如插入、删除,并提供自定义LinkedList的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、链表定义

线性表的链式存储结构的特点是用一组任意的存储单位存储线性表的数据元素,这组存储单位可以是连续的,也可以使不连续的。

种类单链表
【e.g.】Message
单循环链表
【e.g.】丢手绢
双链表
【e.g.】LinkedList
双向循环链表
【e.g.】

二、单链表的定义

节点
数据域:可以有很多内容组成
【e.g.】Message的数据域有:what,arg1,arg2,obj等等

指针域:可以看做是内存地址的引用
【e.g.】Message next; // 下一个节点的引用
数据链
删除一个节点
增加一个节点

三、单链表的应用:MessageQueue

public final class MessageQueue {

    // 添加message, 到消息队列中
    boolean enqueueMessage(Message msg, long when) {
        ...
        Message p = mMessages; // mMessages头指针
        if (p == null || when == 0 || when < p.when) { // 链表为空的时候
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            Message prev;
            for (; ; ) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) { // message 是按照时间when排序的
                    break;
                }
                ...
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }
    }
    
    // 从消息队列中删除一个节点
    Message next() {
        for (;;) {
            Message prevMsg = null;
            Message msg = mMessages; // mMessages头指针
            ...
            if (prevMsg != null) {
                prevMsg.next = msg.next;
            } else {
                mMessages = msg.next;
            }
            msg.next = null; // 一但GC有空,就会把msg回收
        }
    }
}

(一)插入

  • 【图示】定位:查找要插入的位置
 for (; ; ) {
     prev = p;
     p = p.next;
     if (p == null || when < p.when) 
        break;
     ...
  }

在这里插入图片描述

  • 【图示】将msg插入到p之前
msg.next = p; // invariant: p == prev.next
prev.next = msg;

在这里插入图片描述

(二)删除

  • 【图示】删除节点,当prevMsg != null时
if (prevMsg != null) {
    prevMsg.next = msg.next;
}
msg.next = null;

在这里插入图片描述

  • 【图示】删除节点,当prevMsg == null时
if (prevMsg == null) {
    mMessages = msg.next;
}
msg.next = null;

在这里插入图片描述

四、单链表的实操:麻将排序 – 链式基数排序

(一)麻将排序解析

实操:将一副麻将排序。如果用冒泡排序,最坏的情况下时间复杂度:15*15,在服务端进行排序,如果用户量大,那就会特别恶心。可以采用空间换时间的方式解决。

实现逻辑:1、一副牌的点数有9种,9组数据存放这9种点数,第一次循环,将牌的点数归位。
                  2、一副牌的花色有3种,3组数据存放着3种花色,第二次循环,将牌的花色归位。

采用LinkedList,或者使用自定义的单链表,不能使用ArrayList(内部会产生大量的数组拷贝等操作)。运行空间的大小取决于你定义的麻将的大小,和链表没有关系。这个排序算法是最优的。

适用于麻将,斗地主等排序。

  • 初始化
    在这里插入图片描述

  • 第一次循环,将牌的点数归位(总共9个链表,将对应的点数的牌放到对应链表中,然后将9个链表连接在一起)
    在这里插入图片描述

  • 第二次循环,将牌的花色归位(总共3个链表,将对应的点数的牌放到对应链表中,然后将3个链表连接在一起)
    在这里插入图片描述

  • 排序后
    在这里插入图片描述

(二)【代码实现01】Mahjong麻将类

/**
 * 功能:麻将类
 * <p>
 * Created by danke on 2018/11/28.
 */
public class Mahjong {
    /**
     * 花色:万,条,筒
     */
    public int suit;
    /**
     * 点数:一,二,三
     */
    public int rank;

    public static int SUIT_WANG = 1; // 花色:万
    public static int SUIT_TIAO = 2; // 花色:条
    public static int SUIT_TONG = 3; // 花色:筒

    public Mahjong(int suit, int rank) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        if (suit == SUIT_WANG) {
            sb.append("万");
        } else if (suit == SUIT_TIAO) {
            sb.append("条");
        } else if (suit == SUIT_TONG) {
            sb.append("筒");
        }
        sb.append(rank);
        return sb.toString();
    }
}

(三)【代码实现02】逻辑代码

    private int ORDER_BY_SUIT = 0;  // 按照花色排序
    private int ORDER_BY_RANK = 1;  // 按照点数排序
    
    public void radixSort(LinkedList<Mahjong> list) {
        // 1、按照点数分组
        sortGroup(list, ORDER_BY_RANK, 9);
        // 2、按照花色分组
        sortGroup(list, ORDER_BY_SUIT, 3);
    }

    /**
     * 根据对应的orderBy进行分类合并
     *
     * @param list
     * @param orderBy
     * @param groupNumber
     */
    private void sortGroup(LinkedList<Mahjong> list, int orderBy, int groupNumber) {
        LinkedList<Mahjong>[] numList = new LinkedList[groupNumber];
        for (int i = 0; i < numList.length; i++) {
            numList[i] = new LinkedList<>();
        }
        while (!list.isEmpty()) {
            Mahjong remove = list.remove(); // 将麻将取出
            int index = remove.rank - 1; // 下标为点数-1
            if (orderBy == ORDER_BY_SUIT) { // 下标为花色-1
                index = remove.suit - 1;
            }
            numList[index].add(remove); // 放到对应链表中
        }
        // 将链表组拼在一起
        for (int i = 0; i < numList.length; i++) {
            list.addAll(numList[i]);
        }
    }

自己写一个单链表代替JDK提供的LinkedList,性能会最优。

(具体代码见GitHub)

五、双向链表 – 自定义LinedList

对比单向链表和双向链表:

  • 单向链表查询只能从前往后
  • 双向链表查询可以从前往后,也可以从后往前,增加了查询的效率

针对专项问题处理,用自己定义的链式结构要比JDK的性能更好,内存开销小10倍

/**
 * 功能:自己写的LinkedList
 * <p>
 * Created by danke on 2018/12/1.
 */
public class LinkedList<E> {
    transient int size = 0; // 大小
    /**
     * 头结点
     */
    transient Node<E> first; // 前驱
    /**
     * 尾结点
     */
    transient Node<E> last; // 后继

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

    /**
     * 添加数据
     * @param e
     * @return
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    /**
     * 添加到index的位置
     * @param index
     * @param element
     * @return
     */
    public void add(int index, E element) {
        checkPositionIndex(index);
        if (index == size) {
            // 加到最后一位
            linkLast(element);
        } else {
            // 加到指定位置
            linkBefore(element, node(index));
        }
    }

    /**
     * 加到指定位置
     * @param element 需要添加的元素
     * @param succ 指定的节点
     */
    private void linkBefore(E element, Node<E> succ) {
        Node<E> prev = succ.prev;
        Node<E> newNode = new Node<>(prev, element, succ);
        succ.prev = newNode;
        if (prev  == null) {
            first = newNode;
        } else {
            prev.next = newNode;
        }
        size++;
    }

    /**
     * 从尾部添加数据
     * @param e
     */
    private void linkLast(E e) {
        Node<E> l = last;
        Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null) {
            first = newNode;
        } else {
            l.next = newNode;
        }
        size++;
    }

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

    public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null ; x = x.next) {
            result[i++] = x.item;
        }
        return result;
    }

    public E remove() {
        return removeFirst();
    }

    private E removeFirst() {
        final Node<E> f = first;
        if (f == null) {
            throw new NoSuchElementException();
        }
        return unlinkFirst(f);
    }

    private E unlinkFirst(Node<E> f) {
        E element = f.item;
        Node<E> next = f.next;
        f.item = null;
        f.next = null;  // help GC
        first = next;
        if (next == null) {
            last = null;
        } else {
            next.prev = null;
        }
        size--;
        return element;
    }



    public boolean addAll(LinkedList<? extends E> c) {
        return addAll(size, c);
    }

    private boolean addAll(int index, LinkedList<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0) {
            return false;
        }

        Node<E> pred, succ;
        if (index == size) {
            pred = last;
            succ = null;
        } else {
            succ = node(index);
            pred = succ.prev;
        }
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<E>(pred, e, null);
            if (pred == null) {
                first = newNode;
            } else {
                pred.next = newNode;
            }
            pred = newNode;  // 更新最后的节点
        }
        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }
        size += numNew;
        return false;
    }

    /**
     * 获取结点:链式结构查找数据比较困难,需要循环
     * 单向链表和双向链表的区别就在此处:双向链表的查找会比单向链表快
     * @param index
     * @return
     */
    private Node<E> node(int index) {
        if (index < (size >> 1)) { // 如果index在整个链表的前半部分,从前往后找 (size >> 1 = size / 2)
            Node<E> x = first;
            for (int i = 0; i < index; i++) {
                x = x.next;
            }
            return x;
        } else {  // 如果index在整个链表的后半部分,从后往前找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--) {
                x = x.prev;
            }
            return x;
        }
    }

    private void checkElementIndex(int index) {
        if (!(isElementIndex(index))) {
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    }

    /**
     * 判断是否index是否越界
     * @param index
     */
    private void checkPositionIndex(int index) {
        if (!(isPositionIndex(index))) {
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    }

    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

    private String outOfBoundsMsg(int index) {
        return "Index: " + index + ", Size: " + size;
    }

    public int size() {
        return size;
    }

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

    private E unlink(Node<E> x) {
        E item = x.item;
        Node<E> prev = x.prev;
        Node<E> next = x.next;
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        x.item = null;
        size--;
        return item;
    }

    /**
     * 结点
     * @param <E>
     */
    protected static class Node<E> {
        E item; // 数据域
        Node<E> next; // 后继
        Node<E> prev; // 前驱

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

(一)代码解析:尾部添加数据【add】

/**
 * 从尾部添加数据
 * @param e
 */
private void linkLast(E e) {
    Node<E> l = last;
    Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null) {
        first = newNode;
    } else {
        l.next = newNode;
    }
    size++;
}

在这里插入图片描述

(二)代码解析:指定位置上添加数据【add】

/**
 * 加到指定位置
 * @param element 需要添加的元素
 * @param succ 指定的节点
 */
private void linkBefore(E element, Node<E> succ) {
    Node<E> prev = succ.prev;
    Node<E> newNode = new Node<>(prev, element, succ);
    succ.prev = newNode;
    if (prev  == null) {
        first = newNode;
    } else {
        prev.next = newNode;
    }
    size++;
}

在这里插入图片描述

(三)代码解析:删除指定位置的数据【remove】

/**
 * 删除指定位置的元素
 * @param x
 * @return
 */
private E unlink(Node<E> x) {
    E item = x.item;
    Node<E> prev = x.prev;
    Node<E> next = x.next;
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
    x.item = null;
    size--;
    return item;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值