草庐IT

Java之链表(不带头结点,带头结点,迭代实现,递归实现)

允歆辰丶 2023-04-19 原文

目录

一.链表

1.什么是链表

2.链表的分类

 二.不带头结点单向链表的非递归实现

1.接口的定义

2. 不带头结点单向链表的结构

3.链表的添加操作(头插法和尾插法)

1.头插法

2.尾插法

4. 链表的插入操作

5.链表的删除操作

1.删除指定索引的结点

2.删除指定值的第一个结点

3.删除指定值的所有结点

6.链表的其他操作

1.将指定索引的结点的值更换为指定值

2.获得指定索引结点的值

3.判断链表是否包含指定值

4.获得指定值的第一个索引

5.toString方法

三.带头结点单向链表的非递归实现

1.带头结点单向链表的结构

2.链表的添加操作(头插法和尾插法)

1.头插法

2.尾插法

3. 链表的插入操作

4.链表的删除操作

1.删除指定索引的结点

2.删除指定值的第一个结点

3.删除指定值的所有结点

5.链表的其他操作

四.不带头结单向点链表的递归实现

1.基本介绍

2. 链表的插入操作

3.递归打印链表结点的值

1.正序打印

2.逆序打印

4.链表的删除操作

1.删除指定值的所有结点

2.删除链表中的重复值(保留)

3.删除链表中的重复值(不保留)

5.链表的其他操作

五.不带头结点双向链表的非递归实现

1.不带头结点双向链表的结构

2.链表的添加操作(头插法和尾插法)

1.头插法

2.尾插法

3. 链表的插入操作

4.链表的删除操作

1.删除指定索引的结点

2.删除指定值的第一个结点

3.删除指定值的所有结点


一.链表

1.什么是链表

链表是一种 物理存储结构上非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接 次序实现的 。

 

2.链表的分类

链表的类型多种多样,具体可以分为:

1.带头结点的链表和不带头结点的链表

2.单向链表和双向链表

3.循环的和非循环的

 二.不带头结点单向链表的非递归实现

1.接口的定义

在实现具体的方法之前,我们先定义一个接口,之后的这些链表都将实现这些接口,实现这些接口内的方法,主要包含了一些常用的CRUD方法.这个接口我们使用了泛型,之后的链表类也将继承这个接口的泛型,元素的类型就是泛型的类型

public interface SeqList<E> {
    // 在线性表中的最后插入值为element的新元素
    void addLast(E element);

    // 在线性表的前边插入新元素,插入后的元素下标为index
    void addFirst(E element);

    //在索引为index处插入值为element的结点
    void add(int index, E element);

    // 删除当前线性表中索引为index的元素,返回删除的元素值
    E removeByIndex(int index);

    // 删除当前线性表中第一个值为element的元素
    void removeByValue(E element);

    // 删除当前线性表中所有值为element的元素
    void removeAllValue(E element);

    // 将当前线性表中index位置的元素替换为element,返回替换前的元素值
    E set(int index, E element);

    // 得到下标为index结点的值
    E get(int index);

    // 查询链表内是否包含element的结点
    boolean contains(E element);

    // 查询当前线性表中第一个值为element的下标
    int indexOf(E element);

}

2. 不带头结点单向链表的结构

我们这里定义了一个内部类Node,为单向链表的结点类,同时单向链表的属性包含了头结点的引用信息和链表结点个数的定义

public class SingleLinkedList<E> implements SeqList<E> {
    private Node head;  //第一个结点的位置
    private int size;  //链表结点的个数

    private class Node {
        E val;
        Node next;

        public Node(E val) {
            this.val = val;
        }
    }

    @Override
    public void addLast(E element) {

    }

    @Override
    public void addFirst(E element) {

    }

    @Override
    public void add(int index, E element) {

    }

    @Override
    public E removeByIndex(int index) {
        return null;
    }

    @Override
    public void removeByValue(E element) {

    }

    @Override
    public void removeAllValue(E element) {

    }

    @Override
    public E set(int index, E element) {
        return null;
    }

    @Override
    public E get(int index) {
        return null;
    }

    @Override
    public boolean contains(E element) {
        return false;
    }

    @Override
    public int indexOf(E element) {
        return 0;
    }
}

3.链表的添加操作(头插法和尾插法)

1.头插法

    @Override
    public void addFirst(E element) {
        //产生一个值为element的结点
        Node newNode = new Node(element);
        //当前结点的指向头结点
        newNode.next = head;
        //更新head的引用,将其指向新节点
        head = newNode;
        size++;

    }

2.尾插法

    @Override
    public void addLast(E element) {
        Node temp = head;
        if (head == null) {
            head = new Node(element);
            size++;
            return;
        }
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = new Node(element);
        size++;

    }

4. 链表的插入操作

记住:插入和删除的关键是找到插入和删除位置的前驱节点

    @Override
    public void add(int index, E element) {
        //边界的判断
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("get index illegal!");
        }
        //头插
        if (index == 0) {
            addFirst(element);
            return;
        }
        //尾插
        if (index == size) {
            addLast(element);
            return;
        }
        Node temp = head;
        //找到插入位置的前驱节点
        for (int i = 0; i < index - 1; ++i) {
            temp = temp.next;
        }
        Node newNode = new Node(element);
        newNode.next = temp.next;
        temp.next = newNode;
        size++;

    }

    //index的检查
    private boolean rangeCheck(int index) {
        //如果插入的位置索引不合理,返回false
        if (index < 0 || index > size) {
            return false;
        }
        return true;
    }

5.链表的删除操作

1.删除指定索引的结点

    @Override
    public E removeByIndex(int index) {
        //边界的判断
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("get index illegal!");
        }
        //判断头结点的删除
        if (index == 0) {
            Node node = head;
            head = head.next;
            node.next = null;
            size--;
            return node.val;

        }
        //除头结点的输出,找到要删除位置的前驱节点
        Node temp = head;
        for (int i = 0; i < index - 1; ++i) {
            temp = temp.next;
        }
        Node node = temp.next;
        temp.next = temp.next.next;
        node.next = null;
        size--;
        return node.val;
    }

2.删除指定值的第一个结点

    @Override
    public void removeByValue(E element) {
        if (head == null)
            return;
        //删除的恰好是头结点
        if (head.val.equals(element)) {
            head = head.next;
            size--;
            return;
        }
        //删除的是其他节点
        Node temp = head;
        while (temp.next != null) {
            if (temp.next.val.equals(element)) {
                temp.next = temp.next.next;
                size--;
                return;
            }
            temp = temp.next;
        }
        //没有要删除值的结点
        System.out.println("当前链表中不存在值为:" + element + "的结点");


    }

3.删除指定值的所有结点

    @Override
    public void removeAllValue(E element) {
        if (head == null) {
            return;
        }
        //删除的恰好是头结点
        while (head != null && head.val.equals(element)) {
            head = head.next;
            size--;
        }
        //链表全部删完了
        if (head == null) {
            return;
        }
        //删除的是其他节点
        Node temp = head;
        while (temp.next != null) {
            if (temp.next.val.equals(element)) {
                temp.next = temp.next.next;
                size--;
            } else {
                //只有后继节点不是element的时候才能移动
                temp = temp.next;
            }

        }


    }

6.链表的其他操作

1.将指定索引的结点的值更换为指定值

    @Override
    public E set(int index, E element) {
        //边界的判断
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("get index illegal!");
        }
        Node temp = head;
        //找到索引为index的结点
        for (int i = 0; i < index; ++i) {
            temp = temp.next;
        }
        //保存结点的旧值
        E oldValue = temp.val;
        //将索引为index结点的值更换为element
        temp.val = element;
        return oldValue;
    }

2.获得指定索引结点的值

    @Override
    public E get(int index) {
        //边界的判断
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("get index illegal!");
        }
        Node temp = head;
        //找到索引为index的结点
        for (int i = 0; i < index; ++i) {
            temp = temp.next;
        }
        return temp.val;
    }

3.判断链表是否包含指定值

    @Override
    public boolean contains(E element) {
        Node temp = head;
        while (temp != null) {
            if (temp.val.equals(element)) {
                return true;
            }
            temp = temp.next;
        }
        return false;
    }

4.获得指定值的第一个索引

    @Override
    public int indexOf(E element) {
        //头结点为空
        if (head == null)
            return -1;
        Node temp = head;
        for (int i = 0; i < size; ++i) {
            if (temp.val.equals(element)) {
                return i;
            }
            temp = temp.next;
        }
        //表示没有找到值为element的结点的索引
        return -1;
    }

5.toString方法

    @Override
    public String toString() {
        Node temp = head;
        StringBuffer sb = new StringBuffer();
        while (temp != null) {
            sb.append(temp.val + "->");
            temp = temp.next;
        }
        sb.append("NULL");
        return sb.toString();
    }

三.带头结点单向链表的非递归实现

1.带头结点单向链表的结构

带头结点的单链表有一个虚拟头结点,头结点里面存储的值并不是链表中真实的值,没有实际意义,只是作为一个头结点的存在,同时size的值不会因为虚拟头结点的存在加一.

public class SingleLinkedListWithHead<E> implements SeqList<E> {

    private class Node {
        E val;
        Node next;

        public Node(E val) {
            this.val = val;
        }
    }

    private Node dummyHead = new Node(null);
    int size = 0;

    @Override
    public void addFirst(E element) {

    }

    @Override
    public void addLast(E element) {

    }

    @Override
    public void add(int index, E element) {

    }

    @Override
    public E removeByIndex(int index) {
        return null;
    }

    @Override
    public void removeByValue(E element) {

    }

    @Override
    public void removeAllValue(E element) {

    }

    @Override
    public E set(int index, E element) {
        return null;
    }

    @Override
    public E get(int index) {
        return null;
    }

    @Override
    public boolean contains(E element) {
        return false;
    }

    @Override
    public int indexOf(E element) {
        return 0;
    }


}

2.链表的添加操作(头插法和尾插法)

1.头插法

    @Override
    public void addFirst(E element) {
        Node node = new Node(element);
        node.next = dummyHead.next;
        dummyHead.next = node;
        size++;

    }

2.尾插法

    @Override
    public void addLast(E element) {
        Node temp = dummyHead;
        for (int i = 0; i < size; ++i) {
            temp = temp.next;
        }
        Node node = new Node(element);
        temp.next = node;
        size++;
        
    }

3. 链表的插入操作

    @Override
    public void add(int index, E element) {
        //边界的判断
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("get index illegal!");
        }
        if (index == 0) {
            addFirst(element);
            return;
        }
        if (index == size) {
            addLast(element);
            return;
        }
        Node temp = dummyHead;
        Node node = new Node(element);
        for (int i = 0; i < index; ++i) {
            temp = temp.next;
        }
        node.next = temp.next;
        temp.next = node;
        size++;
    }

4.链表的删除操作

1.删除指定索引的结点

    @Override
    public E removeByIndex(int index) {
        //边界的判断
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("get index illegal!");
        }
        Node temp = dummyHead;
        for (int i = 0; i < index; ++i) {
            temp = temp.next;
        }
        Node node = temp.next;
        temp.next = temp.next.next;
        return node.val;
    }

2.删除指定值的第一个结点

    @Override
    public void removeByValue(E element) {
        Node temp = dummyHead;
        while (temp.next != null) {
            if (temp.next.val.equals(element)) {
                temp.next = temp.next.next;
                size--;
                return;
            }
            temp = temp.next;
        }
        System.out.println("当前链表中不存在值为:" + element + "的结点");

    }

3.删除指定值的所有结点

    @Override
    public void removeAllValue(E element) {
        Node temp = dummyHead;
        while (temp.next != null) {
            if (temp.next.val.equals(element)) {
                temp.next = temp.next.next;
                size--;
            } else {
                temp = temp.next;
            }
        }

    }

5.链表的其他操作

这里不对其他操作进行描述,因为大致方法和不带头结点的单链表的操作一致

四.不带头结单向点链表的递归实现

1.基本介绍

递归方式主要实现的是插入操作和删除操作,它的结构和不带头结点的单链表的结构一样

2. 链表的插入操作

    //在索引值为index处插入val的结点
    public void add(int index, int val) {
        head = addInternal(head, index, val);
    }

    private Node addInternal(Node head, int index, int val) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("不合法");
        }
        //头插
        if (index == 0) {
            Node node = new Node(val);
            node.next = head;
            head = node;
            size++;
            return head;
        }
        //index不在头结点插入
        head.next = addInternal(head.next, index - 1, val);
        return head;

    }

3.递归打印链表结点的值

1.正序打印

    //正序打印
    public void print(Node head) {
        if (head == null) {
            System.out.print("NULL");
            return;
        }
        
        System.out.print(head.val + "->");
        print(head.next);
    }

2.逆序打印

    //逆序打印
    public void printReverse(Node head) {
        if (head == null) {
            return;
        }
        printReverse(head.next);
        if (head == this.head) {
            System.out.print(head.val + "->NULL");
        } else {
            System.out.print(head.val + "->");
        }
    }

4.链表的删除操作

1.删除指定值的所有结点

力扣:力扣

    public ListNode removeElements(ListNode head, int val) {
        if (head == null)
            return null;
        head.next = removeElements(head.next, val);
        return head.val == val ? head.next : head;

    }

2.删除链表中的重复值(保留)

力扣:力扣

    public ListNode deleteDuplicates(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        head.next = deleteDuplicates(head.next);
        return head.val == head.next.val ? head.next : head;

    }

3.删除链表中的重复值(不保留)

力扣:力扣

解法一:

    public ListNode deleteDuplicates(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        if (head.val != head.next.val) {
            head.next = deleteDuplicates(head.next);
            return head;
        } else {
            //头结点就是重复的结点
            while (head.next != null && head.val == head.next.val) {
                head = head.next;
            }
            head = deleteDuplicates(head.next);
            return head;
        }

    }

 解法二:

    public ListNode deleteDuplicates(ListNode head) {
        if(head==null||head.next==null){
            return head;
        }
        if(head.val!=head.next.val){
        head.next=deleteDuplicates(head.next);
          return head;
        }else{
            //头结点就是重复的结点
            ListNode cur=head.next;
            while(cur!=null&&cur.val==head.val){
                cur=cur.next;
            }
            return deleteDuplicates(cur);
        }
        
    }

5.链表的其他操作

在这里不做细致的介绍

五.不带头结点双向链表的非递归实现

1.不带头结点双向链表的结构

public class DoubleLinkedList<E> implements SeqList<E> {
    private class DoubleNode {
        E val;
        DoubleNode pre;
        DoubleNode next;

        public DoubleNode(E val) {
            this.val = val;
        }

        public DoubleNode(E val, DoubleNode pre, DoubleNode next) {
            this.val = val;
            this.pre = pre;
            this.next = next;
        }
    }

    private DoubleNode head;
    private DoubleNode tail;
    private int size = 0;


    //尾插法
    @Override
    public void addLast(E element) {
        

    }

    @Override
    public void addFirst(E element) {

    }

    //找到索引为index的位置的结点
    private DoubleNode node(int index) {
        return null;

    }


    @Override
    public void add(int index, E element) {


    }

    @Override
    public E removeByIndex(int index) {
         return null;
    }

    @Override
    public void removeByValue(E element) {

    }

    @Override
    public void removeAllValue(E element) {

    }

    @Override
    public E set(int index, E element) {
        return null;
    }

    @Override
    public E get(int index) {
         return null;
    }

    @Override
    public boolean contains(E element) {
        return false;
    }

    @Override
    public int indexOf(E element) {
        return 0;
    }
}

2.链表的添加操作(头插法和尾插法)

1.头插法

    @Override
    public void addFirst(E element) {
        DoubleNode node = new DoubleNode(element);
        if (tail == null) {
            tail = node;
        } else {
            head.pre = node;
            node.next = head;
        }
        head = node;
        size++;
        
    }

2.尾插法

    //尾插法
    @Override
    public void addLast(E element) {
        DoubleNode node = new DoubleNode(element);
        if (head == null) {
            head = node;
        } else {
            tail.next = node;
            node.pre = tail;
        }
        tail = node;
        size++;

    }

3. 链表的插入操作

    //找到索引为index的位置的结点
    private DoubleNode node(int index) {
        DoubleNode result = null;
        if (index < (size >> 1)) {
            result = head;
            for (int i = 0; i < index; ++i) {
                result = result.next;
            }
        } else {
            result = tail;
            for (int i = size - 1; i > index; --i) {
                result = result.pre;
            }
        }
        return result;


    }

    private boolean rangeCheck(int index) {
        if (index < 0 || index > size) {
            return false;
        }
        return true;

    }


    @Override
    public void add(int index, E element) {
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("illegal index!!!");
        }
        if (index == 0) {
            addFirst(element);
        }
        if (index == size) {
            addLast(element);
        }
        DoubleNode pre = node(index - 1);
        DoubleNode next = pre.next;
        DoubleNode node = new DoubleNode(element, pre, next);
        pre.next = node;
        next.pre = node;
        size++;

    }

4.链表的删除操作

1.删除指定索引的结点

    //在当前链表中删除node结点
    private DoubleNode unlink(DoubleNode node) {
        DoubleNode pre = node.pre;
        DoubleNode next = node.next;
        // 先处理左半区域
        if (pre == null) {
            this.head = next;
        } else {
            node.pre = null;
            pre.next = next;
        }
        // 在处理右半区域
        if (next == null) {
            this.tail = pre;
        } else {
            node.next = null;
            next.pre = pre;
        }
        size--;
        return node;

    }

    @Override
    public E removeByIndex(int index) {
        if (!rangeCheck(index)) {
            throw new IllegalArgumentException("illegal index!!");
        }
        DoubleNode node = node(index);
        return unlink(node).val;
    }

2.删除指定值的第一个结点

    @Override
    public void removeByValue(E element) {
        DoubleNode node = head;
        for (int i = 0; i < size; i++) {
            if (node.val.equals(element)) {
                unlink(node);
                return;
            }
            node = node.next;
        }

    }

3.删除指定值的所有结点

    @Override
    public void removeAllValue(E element) {
        DoubleNode node = head;
        // 因为每次unlink之后都会修改size的值,但是删除所有元素,
        // 要把所有链表节点全部遍历一遍
        int length = this.size;
        for (int i = 0; i < length; i++) {
            DoubleNode next = node.next;
            if (node.val.equals(element)) {
                unlink(node);
            }
            node = next;
        }
    }

有关Java之链表(不带头结点,带头结点,迭代实现,递归实现)的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. ruby - 为什么 Ruby 的 each 迭代器先执行? - 2

    我在用Ruby执行简单任务时遇到了一件奇怪的事情。我只想用每个方法迭代字母表,但迭代在执行中先进行:alfawit=("a".."z")puts"That'sanalphabet:\n\n#{alfawit.each{|litera|putslitera}}"这段代码的结果是:(缩写)abc⋮xyzThat'sanalphabet:a..z知道为什么它会这样工作或者我做错了什么吗?提前致谢。 最佳答案 因为您的each调用被插入到在固定字符串之前执行的字符串文字中。此外,each返回一个Enumerable,实际上您甚至打印它。试试

  4. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  5. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  6. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  7. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  8. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  9. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  10. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

随机推荐