Java 链表API

Java 链表

1、什么是链表?

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

每一个链表都包含多个节点,节点又包含两个部分:

1)一个是数据域(储存节点含有的信息)

2)一个是引用域(储存下一个节点或者上一个节点的地址)

链表的理解示意图

 

 

2、链表的特点是什么?

获取数据麻烦,需要遍历查找,比数组慢

方便插入、删除

 

3、链表的实现原理

创建一个节点类,其中节点类包含两个部分,第一个是数据域(你到时候要往节点里面储存的信息),第二个是引用域(相当于指针,单向链表有一个指针,指向下一个节点;双向链表有两个指针,分别指向下一个和上一个节点)

创建一个链表类,其中链表类包含三个属性:头结点、尾节点和大小,方法包含添加、删除、插入等等方法。

 

单向链表的节点类:

1
2
3
4
5
6
7
8
public class Node {
    public Object data;
    public Node next;
 
    public Node(Object e){
        this.data = e;
    }
}

 

双向链表的节点类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Node {
    public Object e;
    public Node next;
    public Node pre;
    public Node(){
 
    }
    public Node(Object e){
        this.e = e;
        next = null;
        pre = null;
    }
}

 

4、如何自己写出一个链表?

代码如下(以双向链表为例,有详细的注释,单向链表同理):

1)首先,创建了一个节点类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package MutuLink;
 
public class Node {
    public Object e;
    public Node next;
    public Node pre;
    public Node(){
 
    }
    public Node(Object e){
        this.e = e;
        next = null;
        pre = null;
    }
}

 

2)然后,创建了一个链表类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package MutuLink;
 
public class MyList {
    private Node head;
    private Node tail;
    private int size = 0;
 
    public MyList() {
        head = new Node();
        tail = new Node();
        head.next =null;
        tail.pre = null;
    }
 
    public boolean empty() {
        if (head.next == null)
            return true;
        return false;
    }
    //找到所找下标节点的前一个节点
    public Node findpre(int index){
        Node rnode = head;
        int dex = -1;
        while(rnode.next != null){
            //找到了插入节点的上一个节点
            if( dex== index - 1){
                return rnode;
            }
            rnode = rnode.next;
            dex++;
        }
        return null;
    }
    public Node findthis(int index){
        Node rnode = head;
        //把rnode想象为指针,dex为指向的下标,这个地方很容易错,因为当指向最后一个节点时没有判断IF就跳出循环了
        int dex = -1;
        while(rnode.next != null){
            if(dex == index)
            return rnode;
            rnode = rnode.next;
            dex++;
        }
        if(dex == size - 1){
            return rnode;
        }
//        Node test = new Node(new Students("haha",1,2));
        return null;
    }
 
    // 往链表末尾加入节点
    public void add(Object e) {
        Node node = new Node(e);
        Node rnode = head;
        //如果是空链表的话插入一个节点,这个节点的pre不能指向上一个节点,必须指空
        if (this.empty()) {
            rnode.next = node;
            rnode.next.pre = null;
            tail.pre = node;
            size++;
        } else {
            while (rnode.next != null)
                rnode = rnode.next;
            rnode.next = node;
            node.pre = rnode;
            tail.pre = node;
            size++;
        }
    }
    //往链表的某一个标插入一个节点
    public boolean add(int index,Object e){
        if(index <0||index>=size)
            return false;
        Node node = new Node(e);
        Node prenode = this.findpre(index);
        node.next = prenode.next;
        prenode.next.pre = node;
        prenode.next = node;
        node.pre = prenode;
        size++;
        return true;
    }
    public boolean add(int index,MyList myl){
        if(index <0 || index >= size)
            return false;
        Node prenode = this.findpre(index);
//        myl.tail.pre.next = prenode.next;
//        prenode.pre = myl.tail.pre;
//        tail.pre = null;
//        prenode.next = myl.head.next;
//        myl.head.next.pre = prenode;
//        head.next = null;
        myl.tail.pre.next = prenode.next;
        prenode.next.pre = myl.tail.pre.pre;
        myl.head.next.pre = prenode.pre;
        prenode.next = myl.head.next;
        myl.head = null;
        myl.tail = null;
        size+=myl.size;
        return true;
    }
 
    public Object remove(int index){
        Object ob= this.get(index);
        if(index <0 || index >= size)
            return null;
        //特殊情况,当移除节点是最后一个节点的时候
        //较为复杂通过画图来写代码
        if(index == size - 1){
            Node prenode = this.findpre(index);
            this.tail.pre = this.tail.pre.pre;
            this.tail.pre.next.pre = null;
            this.tail.pre.next =null;
            size--;
            return ob;
        }
        //比较复杂,通过画图解决
        else{
            Node prenode = this.findpre(index);
            prenode.next = prenode.next.next;
            prenode.next.pre.next = null;
            prenode.next.pre = prenode.next.pre.pre;
            size--;
            return ob;
        }
    }
 
 
    public Object get(int index){
        Node thisnode = this.findthis(index);
        return thisnode.e;
    }
    public int size(){
        return size;
    }
}

 

3)最后,测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package MutuLink;
 
import java.util.Random;
 
public class manage {
    public static void main(String[] args) {
        String name = "";
        int credit;
        int age;
        int size;
        MyList myl = new MyList();
        Random random = new Random();
        size = random.nextInt(5) + 1;
        for (int i = 0; i < size; i++) {
            credit = random.nextInt(5);
            age = random.nextInt(5) + 18;
            for (int j = 0; j < 4; j++) {
                name += (char) (random.nextInt(26) + 97);
            }
            Students stu = new Students(name, credit, age);
            myl.add(stu);
            name = "";
        }
 
        System.out.println("Size of myl1 is "+ myl.size());
        for(int i = 0; i < myl.size() ;i++){
            Students stu2 = (Students) myl.get(i);
            stu2.show();
        }
//        //测试能否在链表末尾加入节点(成功)
//        for(int i = 0; i < myl.size() ;i++){
//            Students stu2 = (Students) myl.get(i);
//            stu2.show();
//        }
//        //测试能否通过下标加入一个节点(成功)
//        Students stu3 = new Students("cyt",5,18);
//        myl.add(1, stu3);
//        System.out.println("Size is "+ myl.size());
//        for(int i = 0; i < myl.size() ;i++){
//            Students stu2 = (Students) myl.get(i);
//            stu2.show();
//        }
 
        MyList myl2 = new MyList();
        size = random.nextInt(5) + 1;
        for (int i = 0; i < size; i++) {
            credit = random.nextInt(5);
            age = random.nextInt(5) + 18;
            for (int j = 0; j < 4; j++) {
                name += (char) (random.nextInt(26) + 97);
            }
            Students stu2 = new Students(name, credit, age);
            myl2.add(stu2);
            name = "";
        }
        System.out.println("Size is of myl2 "+ myl2.size());
        for(int i = 0; i < myl2.size() ;i++){
            Students stu2 = (Students) myl2.get(i);
            stu2.show();
        }
 
        myl.add(1, myl2);
        System.out.println("Size is of myl1 "+ myl.size());
        for(int i = 0; i < myl.size() ;i++){
            Students stu2 = (Students) myl.get(i);
            stu2.show();
        }
    }
}

 

4)结果输出

Java 实现链表类 LinkList

顺序数组表作为数据存储结构有很多优点,最重要的就是顺序表的元素是随机存取的。

但是顺序表也有很多缺点,主要的缺点有两个:

一是插入和删除的效率低下,因为插入和删除操作需要变化部分元素的位置,时间复杂度是O(n)。

二是数据一旦创建就不能改变大小,如果想扩大数组的容量,那么应该创建一个新的更大的新数组,将原数组复制到新数组中,这是十分不方便的。

 

为了解决上述问题,链表就出现了,链表的主要特点是:

1、链表的大小是动态的,随着数据的增加或者删除动态改变长度。

2、链表的插入、删除十分高效,对于经常需要插入和删除数据的应用,非常适合使用链表作为数据存储结构。

顺序表和链表都属于线性表。

 

Java 链表API

在看代码之前,我们先看看链表需要完成什么任务,也就是链表的API,

这样有助于理解代码,也有助于快速查找自己想实现的功能。

  • LinkList(): 构造函数
  • clear(): 删除整个列表
  • removeAll(): 删除整个列表,调用clear函数来实现
  • getNode(int i): 获得逻辑上i号节点,返回节点
  • get(int i): 获得逻辑上i号节点的值,返回值
  • set(int i,T x): 修改i号节点的值
  • add(int i,T x): 将值为x的节点插入到i号位置
  • add(T key): 在链表尾部插入元素key
  • addBack(T key): 在链表尾部插入元素key,和add(T key)函数的作用一样
  • addFront(T key): 在链表首部插入元素key
  • remove(int i): 删除i号节点,并返回i号节点对应的值
  • remove(): 重载remove,删除头节点
  • removeFront(): 删除链表头节点,与remove()函数同义
  • removeBack(): 删除链表尾节点
  • addSort(T value): 将值value按从小到大的排序方式插入链表
  • sort(): 对链表按照从小到大的顺序进行排序
  • indexOf(int begin,int end,T key): 在索引begin和end之间查找值key,返回逻辑编号
  • search(T key): 功能同indexOf,遍历整个链表,一般不使用,主要用于实现字典
  • contains(T key): 判断链表中是否存在值为key节点
  • toString(): 将链表中的值转换成字符串
  • toArray(): 将链表转换成Object数组
  • toArray(E[] a): 将单链表转化为特定类型的数组,使用了函数泛型
  • iterator(): 返回迭代器对象

 

Java 链表代码

要注意的点:

1、使用代码的时候,记得把package改成自己的文件所在的包名。

2、代码中LinkList继承的父类AbsList在这里没有写,因为写代码时,在顺序表SeqList中实现了AbsList类,这个AbsList默认是default,也就是包访问权限(default修饰的类是包访问的),然后我把SeqList和LinkList两个类放在一个包中了。所以其实LinkList使用的AbsList其实是SeqList类中的AbsList,所以这里就使用的是上篇文章中的SeqList中的AbsList,所以如果想让这个类可以使用,去上面文章中找AbsList类copy一下就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
/*
 * created on April 16 14:40 2019
 *
 * @author:lhy
 */
package DS;
 
import java.util.Iterator;
 
 
//创建节点类Lnode<T>
class Lnode<T> implements Comparable<Lnode<T>> {
    public T data;  // 节点值
    public Lnode<T> next; // 保存下个节点
 
    // Lnode<T>构造函数
    public Lnode(T key) {
        data = key;
        next = null;
    }
 
    public Lnode(T key, Lnode<T> next) {
        data = key;
        this.next = next;
    }
 
    // 判断节点值是否相等
    public boolean equals(Object e) {
        Lnode<T> node = (Lnode<T>) e;
        return data.equals(node.data);
    }
 
    // 实现Comparable的compareTo方法,用于比较链表节点比较大小
    public int compareTo(Lnode<T> e) {
        Comparable<T> x;
        if(data instanceof Comparable) {
            x=(Comparable<T>)data;
            return (int)x.compareTo(e.data);
        }
        else {
            throw new ClassCastException("类型无法比较");
        }
    }
     
    //将该节点的值变成字符串格式
    public String toString() {
        return data.toString();
    }
}
 
/*
 * LinkList API介绍
 *
 * LinkList(): 构造函数
 * clear(): 删除整个列表
 * removeAll(): 删除整个列表,调用clear函数来实现
 * getNode(int i): 获得逻辑上i号节点
 * get(int i): 获得逻辑上i号节点的值
 * set(int i,T x): 修改i号节点的值
 * add(int i,T x): 将值为x的节点插入到i号位置
 * add(T key): 在链表尾部插入元素key
 * addBack(T key): 在链表尾部插入元素key,和add(T key)函数的作用一样
 * addFront(T key): 在链表首部插入元素key
 * remove(int i): 删除i号节点,并返回i号节点对应的值
 * remove(): 重载remove,删除头节点
 * removeFront(): 删除链表头节点,与remove()函数同义
 * removeBack(): 删除链表尾节点
 * addSort(T value): 将值value按从小到大的排序方式插入链表
 * sort(): 对链表按照从小到大的顺序进行排序
 * indexOf(int begin,int end,T key): 在索引begin和end之间查找值key,返回逻辑编号
 * search(T key): 功能同indexOf,遍历整个链表,一般不使用,主要用于实现字典
 * contains(T key): 判断链表中是否存在值为key节点
 * toString(): 将链表中的值转换成字符串
 * toArray(): 将链表转换成Object数组
 * toArray(E[] a): 将单链表转化为特定类型的数组,使用了函数泛型
 * iterator(): 返回迭代器对象
 */
public class LinkList<T> extends AbsList<T> implements Iterable<T> {
    //LinkList继承了SeqList中的AbsList抽象类,继承了Iterable接口
    Lnode<T> first=null,last=null; //头指针和尾指针
    Iterator<T> iterator=null; //指向当前节点的迭代器
    //构造函数
    public LinkList() {
        first=last=null;
        length=0;
        this.iterator=new LinkIterator();
    }
     
    //比较器,供内部使用
    private int compare(Lnode<T> a,Lnode<T> b) {
        return a.compareTo(b);
    }
    //删除整个列表
    public void clear() {
        first=last=null;
        length=0;
    }
     
    //删除整个列表,调用clear函数来实现
    public void removeAll() {
        clear();
    }
     
    //获得逻辑上的i号节点
    public Lnode<T> getNode(int i){
        //判断i号节点是否越界
        if(i<0||i>length-1) {
            return null;
        }
        //判断i号是否为头节点(其实可以不用判断)
        if(i==0) {
            return first;
        }
        else {
            Lnode<T> p=first;
            int j=0;
            while (p!=null&&j<i) {
                p=p.next;
                j++;
            }
            return p;
        }
         
    }
     
    //获得i号节点的值,调用getNode完成具体工作
    public T get(int i) {
        Lnode<T> pLnode=getNode(i);
        if(pLnode==null)
            return null;
        else
            return pLnode.data;
    }
     
    //修改i号节点的值
    public boolean set(int i,T x) {
        Lnode<T> p=getNode(i);
        if(p==null) {
            return false;
        }
        else {
            p.data=x;
            return true;
        }
    }
     
    //将值为x的节点插入到i号位置
    public void add(int i,T x) {
        Lnode<T> p,s;
        int j=i-1;
        s=new Lnode<T>(x,null);
        //在空链表中插入节点s
        if(first==null||length==0) {
            first=s;
            last=s;
        }
         
        //在头节点之前插入节点s,即i=0时
        else if(j<0) {
            s.next=first;
            first=s;
        }
        //在链表尾部插入节点s
        else if(j>=length-1) {
            last.next=s;
            last=s;
        }
        //在链表中间插入节点s
        else {
            p=getNode(j);
            s.next=p.next;
            p.next=s;
        }
        length++;
    }
     
    //重载add()函数,在链表尾部插入元素key
    public void add(T key) {
        add(length,key);
    }
     
    //在链表尾部插入元素key,和add(T key)函数的作用一样
    public void addBack(T key) {
        add(length,key);
    }
     
    //在链表首部插入元素key
    public void addFront(T key) {
        add(0,key);
    }
     
    //删除i号节点,并返回i号节点对应的值,通过调用removeNode实现
    public T remove(int i) {
        Lnode<T> p=removeNode(i);
        if(p!=null)
            return p.data;
        else
            return null;
    }
     
    //核心算法,删除逻辑上的i号节点,内部使用,返回Lnode<T>
    protected Lnode<T> removeNode(int i){
        Lnode<T> p,q;
        //判断链表是否为空
        if(first==null) {
            return null;
        }
        //删除头节点
        if(i==0) {
            p=first;
            first=first.next;
            length-=1;
            return p;
        }
        //删除中间节点或尾节点
        if(i>=1 && i<=length-1) {
            //首先获得i-1位置所在的节点
            p=getNode(i-1);
            //获得i节点位置上的节点
            q=p.next;
            p.next=q.next;
            if(q==last) {
                last=p;
            }
            length--;
            return q;
        }
        return null;
    }
     
    //重载remove,删除头节点
    public T remove() {
        return removeNode(0).data;
    }
     
    //删除头节点,与remove()函数同义
    public T removeFront() {
        return removeNode(0).data;
    }
     
    //删除尾节点
    public T removeBack() {
        return removeNode(length-1).data;
    }
     
    //将值value按从小到大的排序方式插入链表,调用insertOrder实现
    public void addSort(T value) {
        Lnode<T> s=new Lnode(value);
        insertOrder(s);
    }
     
    //有序插入的核心算法
    private void insertOrder(Lnode<T> s) {
        Lnode<T> p1,p2;
        length++;
        //在空链表中插入节点
        if(first==null) {
            first=s;
            last=first;
            return ;
        }
        //小于头节点的值,将节点插入到头节点之前
        if(compare(s, first)<0) {
            s.next=first;
            first=s;
            return ;
        }
        //大于最后一个节点值,将节点在链表尾部插入
        if(compare(s, last)>0) {
            last.next=s;
            last=s;
            return ;
        }
        //被插入的节点p在p1和p2之间,p1在前,p2在后
        p2=first;
        p1=p2;
        while(p2!=null) {
            //s节点比p2节点大时,将p1等于p2,p2后移一个位置,直到s小于等于p2时停止循环,这时s节点的值在p1和p2之间
            if(compare(s, p2)>0) {
                p1=p2;
                p2=p2.next;
            }
            else {
                break;
            }
        }
        s.next=p2;
        p1.next=s;
        return;
    }
     
    //对链表排序
    public void sort() {
        LinkList<T> sl=new LinkList<T>();//创建一个存放有序序列的链表
        Lnode<T> p;
        p=this.removeNode(0);//取出无序链表的头节点
        while(p!=null) {
            sl.addSort(p.data);
            p=this.removeNode(0);
        }
        Lnode<T> q=sl.first;
        while(q!=null) {
            this.add(q.data);
            q=q.next;
        }
    }
     
    //在索引begin和end之间查找值key,返回逻辑编号
    public int indexOf(int begin,int end,T key) {
        Lnode<T> p=getNode(begin);//获取开始节点
        int i=begin;
        while(p!=null&&i<end) {
            if(p.data.equals(key))
                return i;
            p=p.next;
            i++;
        }
        return -1;
    }
     
    //功能同indexOf,一般不使用,主要用于实现字典
    public T search(T key) {
        Lnode<T> p=getNode(0);
        while(p!=null) {
            if(p.data.equals(key)) {
                return p.data;
            }
            p=p.next;
        }
        return null;
    }
     
    //判断链表中是否存在值为key节点
    public boolean contains(T key) {
        if(indexOf(0,length,key)==-1) return false;
        else return true;
    }
     
    //将链表中的值转换成字符串
    public String toString() {
        String string;
        Lnode<T> pLnode;
        pLnode=first;
        string="(";
        while(pLnode!=null) {
            string+=pLnode.data.toString()+" ";
            pLnode=pLnode.next;
        }
        return string+")";
    }
     
    //将链表转换成Object数组
    public Object[] toArray() {
        Object[] a=new Object[length];
        Lnode<T> pLnode=first;
        for(int i=0;i<length;i++) {
            a[i]=pLnode.data;
            pLnode=pLnode.next;
        }
        return a;
    }
     
    //将单链表转化为特定类型的数组,使用了函数泛型
    public <E> E[] toArray(E[] a) {
        if(a.length<length) {
            //创建一个长度为length(和链表长度相等),类型为数组a的元素类型的数组,并强制转换成E[]类型
            a=(E[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), length);
        }
        int i=0;
        Object[] result=a;
        Lnode<T> xLnode=this.first;
        for(i=0;i<length;i++) {
            result[i]=xLnode.data;
            xLnode=xLnode.next;
        }
        if(a.length>length) {
            a[length]=null;
        }
        return a;
    }
     
    //返回迭代器对象
    public Iterator<T> iterator(){
        return new LinkIterator();
    }
     
    //内部类,实现迭代器
    private class LinkIterator implements Iterator<T>{
        private int index=0;
        private Lnode<T> current=first;
        public boolean hasNext() {
            //在调用next()之后,index自增,确保index不等于person的长度
            return (index!=length() && current!=null);
        }
         
        public T next() {
            T temp=current.data;
            current=current.next;
            index++;
            return temp;
        }
         
        public int nextIndex() {
            return index++;//先返回index的值,后加1
        }
        public void remove() {
            //未实现本方法
        }
    }
}

 

使用链表示例

创建一个LinkList链表对象,对LinkList类中各个函数进行实现检验,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
 * created on April 16 15:40 2019
 *
 * @author:lhy
 */
package DS;
 
import java.util.Iterator;
 
/*
 * LinkList API介绍
 *
 * LinkList(): 构造函数
 * clear(): 删除整个列表
 * removeAll(): 删除整个列表,调用clear函数来实现
 * get(int i): 获得逻辑上i号节点的值
 * set(int i,T x): 修改i号节点的值
 * add(int i,T x): 将值为x的节点插入到i号位置
 * add(T key): 在链表尾部插入元素key
 * addBack(T key): 在链表尾部插入元素key,和add(T key)函数的作用一样
 * addFront(T key): 在链表首部插入元素key
 * remove(int i): 删除i号节点,并返回i号节点对应的值
 * remove(): 重载remove,删除头节点
 * removeFront(): 删除链表头节点,与remove()函数同义
 * removeBack(): 删除链表尾节点
 * addSort(T value): 将值value按从小到大的排序方式插入链表
 * sort(): 对链表按照从小到大的顺序进行排序
 * indexOf(int begin,int end,T key): 在索引begin和end之间查找值key,返回逻辑编号
 * search(T key): 功能同indexOf,遍历整个链表,一般不使用,主要用于实现字典
 * contains(T key): 判断链表中是否存在值为key节点
 * toString(): 将链表中的值转换成字符串
 * toArray(): 将链表转换成Object数组
 * toArray(E[] a): 将单链表转化为特定类型的数组,使用了函数泛型
 * iterator(): 返回迭代器对象
 */
 
public class Test_LinkList {
    public static void main(String[] args) {
        LinkList<Integer> linkList=new LinkList<Integer>();
        //对linkList赋值
        for(int i=0;i<10;i++) {
            linkList.add((int)(Math.random()*1000));
        }
        System.out.println("由系统随机数生成的链表为:"+linkList.toString());
        System.out.println("获取7号节点的链表元素为:"+linkList.get(7));
        //将下标为1的节点值修改为2333
        linkList.set(1,2333);
        System.out.println("将1号节点值修改为2333,然后输出:"+linkList.toString());
        //向3号位置插入值为5555的节点
        linkList.add(3,5555);
        System.out.println("向3号节点位置插入值为5555的节点,输出链表:"+linkList.toString());
        //向链表尾部插入值为9999的节点
        linkList.add(9999);
        System.out.println("向链表尾部插入值为9999的节点,输出链表:"+linkList.toString());
        //向链表首部插入值为12345的节点
        linkList.addFront(12345);
        System.out.println("向链表首部插入值为12345的节点,输出链表:"+linkList.toString());
        System.out.println("删除4号节点,返回值为:"+linkList.remove(4));
        System.out.println("删除4号节点后,输出链表"+linkList.toString());
        //删除尾节点
        linkList.removeBack();
        System.out.println("删除尾节点后,输出链表:"+linkList.toString());
        //对linkList进行排序
        linkList.sort();
        System.out.println("排序后的链表为:"+linkList.toString());
        //向链表中有序插入节点值为666的节点
        linkList.addSort(666);
        System.out.println("向链表中有序插入节点值为666的节点后,输出链表:"+linkList.toString());
        System.out.println("查找值为666的节点在链表中位置为:"+linkList.indexOf(0, linkList.length,666));
        System.out.println("判断链表中是否有值为665的节点:"+linkList.contains(665));
        //获得一个链表迭代器对象
        Iterator<Integer> iterator=linkList.iterator();
        //使用迭代器输出链表元素
        System.out.print("使用迭代器输出链表元素:");
        while(iterator.hasNext()) {
            System.out.print(iterator.next()+" ");
        }
        //清空链表
        linkList.clear();
        System.out.println("\n输出清空后的链表:"+linkList.toString());
 
    }
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
由系统随机数生成的链表为:(834 738 152 568 863 608 495 975 742 475 )
获取7号节点的链表元素为:975
1号节点值修改为2333,然后输出:(834 2333 152 568 863 608 495 975 742 475 )
3号节点位置插入值为5555的节点,输出链表:(834 2333 152 5555 568 863 608 495 975 742 475 )
向链表尾部插入值为9999的节点,输出链表:(834 2333 152 5555 568 863 608 495 975 742 475 9999 )
向链表首部插入值为12345的节点,输出链表:(12345 834 2333 152 5555 568 863 608 495 975 742 475 9999 )
删除4号节点,返回值为:5555
删除4号节点后,输出链表(12345 834 2333 152 568 863 608 495 975 742 475 9999 )
删除尾节点后,输出链表:(12345 834 2333 152 568 863 608 495 975 742 475 )
排序后的链表为:(152 475 495 568 608 742 834 863 975 2333 12345 )
向链表中有序插入节点值为666的节点后,输出链表:(152 475 495 568 608 666 742 834 863 975 2333 12345 )
查找值为666的节点在链表中位置为:5
判断链表中是否有值为665的节点:false
使用迭代器输出链表元素:152 475 495 568 608 666 742 834 863 975 2333 12345
输出清空后的链表:()

 

 

Java 实现顺序表类 SeqList

数据结构中的线性存储结构分为两大类:顺序存储链式存储

顺序存储对应的是顺序表数组,链式存储对应的是链表。

这篇文章主要介绍如何使用Java实现一个顺序表。

 

顺序表介绍

顺序表: 把线性表的元素按照一定逻辑顺序依次放在一组地址连续的存储单元里,用这种方式存储的线性表简称为 顺序表。而链表中的元素是没有放在一组地址连续的存储单元里,它们之间的链接是靠指针串一起的。

优点:随机存取表中元素。随机存取的意思是对所有元素序号来讲都是一样的计算时间,也就是说,访问任何一个元素的时间都是相同的。由于每个元素在计算机中占有的存储单元是相同的(假设占有的空间是c),则可知a[i]所在的存储地址是: Loc(ai)=Loc(a0)+i*c,访问不同存储位置的元素时间都是相同的,因此顺序标是随机存取。

缺点:插入和删除比较慢,不可以增加长度,如想增加长度,则需另开内存将原数组复制到新空间。

 

Java 顺序表API

先看看代码的API,有助于理解代码(虽说代码中的注释很完整。。。):

  • setIncr(int inc): 设置顺序表每次增加时增量的大小
  • setCapacity(int newSize): 设置新的最大容量
  • getCapacity(): 获取数组的最大容量
  • size(): 获取顺序表已经存放数据的数量,同length()
  • get(int i): 获取顺序表下标为i的元素值
  • set(int i,T x): 修改下标值为i的元素值,即为修改data[i]
  • indexOf(int begin,int end,T value): 在begin和end之间查找值为value的元素下标值,使用顺序查找法
  • indexOf(T o): 在整个线性表中查找元素o的下标值
  • indexOf(int begin,T o): 在下标为begin到末尾的线性表中查找元素o的下标值
  • add(int i,T x): 在i位置插入数据元素x
  • add(T x): 在表尾添加数据元素x
  • append(T x): 在表尾添加数据元素x
  • addSort(T x): 以有序方式向顺序表中插入数据元素x
  • sort(): 对顺序表从小到大排序
  • remove(int i): 删除下标为i的元素,并返回被删除的元素
  • remove(T value): 删除值为value的元素,并返回被删除的元素
  • clear(): 清除整个顺序表
  • toString(): 将顺序表转化成字符串,便于直接输出顺序表
  • toArray(): 将顺序表转化成Object数组,并返回
  • toArray(T[] a): 将顺序表转换为类型为E的数组
  • iterator(): 返回一个迭代对象
  • length(): 获取线性表长度
  • isEmpty(): 判断线性表是否为空

 

Java 顺序表代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/*
 * created on April 15 8:20 2019
 *
 * @author:lhy
 */
 
package DS;
 
import java.util.Arrays;
import java.util.Iterator;
 
import org.w3c.dom.html.HTMLIsIndexElement;
 
interface MyList<T>{
    //接口中每一个方法都是隐式抽象的,接口中所有方法会被隐式指定为public abstract类型(且只能是public abstract)
    //接口中的变量会被隐式指定为public static final类型变量
    //接口中的函数不能在接口中实现,只能由实现接口的类来实现接口中的方法
     
    boolean isEmpty();//判断线性表是否为空
    int length();//获得List的元素个数
    T get(int i);//取得第i个位置上的数据元素,返回数据元素的值
    boolean set(int i,T x);//将第i个位置上的数据元素设置为值x
    void add(int i,T x);//位置i及其后的元素依次后移一个位置,然后将x插入到第i个位置
    T remove(int i);//删除第i个位置上的数据元素,位置i+1及其后的元素依次前移一个位置
    int indexOf(T x);//获取数据元素x在表中的下标位置
}
 
abstract class AbsList<T> implements Iterable<T>,MyList<T>{
    //必须实现接口MyList中所描述的所有方法,否则必须声明为抽象类
    protected int length;//length指的是这个顺序表已经容纳的元素个数
    abstract public T get(int i);//返回第i个元素
    abstract public boolean set(int i,T x);//设置第i个元素的值为x
    abstract public int indexOf(int begin,int end,T o);//在begin和end之间寻找值为o的元素的下标
    abstract public void add(int i,T x);//插入x作为第i个元素
    abstract public void clear();//清空列表
    abstract public T remove(int i);//删除第i个元素并返回被删除对象
    abstract public Iterator<T> iterator();//返回一个迭代器
     
    //判断线性表是否为空
    public boolean isEmpty() {
        return length==0;
    }
    //获取线性表长度
    public int length(){
        return length;
    }
    //在线性表最后插入x元素
    public void add(T x) {
        add(length,x);
    }
    //与add一样
    public void append(T x) {
        add(length,x);
    }
    //在整个线性表中查找元素o的下标值
    public int indexOf(T o) {
        return indexOf(0,length,o);
    }
    //在下标为begin到末尾的线性表中查找元素o的下标值
    public int indexOf(int begin,T o) {
        return indexOf(begin,length,o);
    }
    //删除第i个元素并返回删除对象
    public T remove(T o) {
        return remove(indexOf(o));
    }
}
 
/*
 * SeqList API介绍
 * setIncr(int inc): 设置顺序表每次增加时增量的大小
 * setCapacity(int newSize): 设置新的最大容量
 * getCapacity(): 获取数组的最大容量
 * size(): 获取顺序表已经存放数据的数量,同length()
 * get(int i): 获取顺序表下标为i的元素值
 * set(int i,T x): 修改下标值为i的元素值,即为修改data[i]
 * indexOf(int begin,int end,T value): 在begin和end之间查找值为value的元素下标值,使用顺序查找法
 * indexOf(T o): 在整个线性表中查找元素o的下标值
 * indexOf(int begin,T o): 在下标为begin到末尾的线性表中查找元素o的下标值
 * add(int i,T x): 在i位置插入数据元素x
 * add(T x): 在表尾添加数据元素x
 * append(T x): 在表尾添加数据元素x
 * addSort(T x): 以有序方式向顺序表中插入数据元素x
 * sort(): 对顺序表从小到大排序
 * remove(int i): 删除下标为i的元素,并返回被删除的元素
 * remove(T value): 删除值为value的元素,并返回被删除的元素
 * clear(): 清除整个顺序表
 * toString(): 将顺序表转化成字符串,便于直接输出顺序表
 * toArray(): 将顺序表转化成Object数组,并返回
 * toArray(T[] a): 将顺序表转换为类型为E的数组
 * iterator(): 返回一个迭代对象
 * length(): 获取线性表长度
 * isEmpty(): 判断线性表是否为空
 */
 
public class SeqList<T> extends AbsList<T> implements Iterable<T> {
    //必须实现抽象类AbsList中的所有抽象方法
    private int incrementSize;//顺序表每次增量的长度
    protected Object[] data;//保存顺序表数据的数组
    //默认构造函数
    public SeqList() {
        this(16);
    }
    //构造函数,设定初始容量为initLen
    public SeqList(int initLen) {
        if(initLen<=0) {
            initLen=16;
        }
        length=0;
        incrementSize=16;
        data=new Object[initLen];
    }
    //构造函数,用elem数组初始化顺序表
    public SeqList(T[] elem) {
        length=elem.length;
        incrementSize=16;
        data=Arrays.copyOf(elem, length);
    }
    //设置顺序表每次容量增加时增量的大小,默认是16
    public void setIncr(int inc) {
        incrementSize=inc;
    }
    //设置新的数组容量
    public void setCapacity(int newSize) {
        data=Arrays.copyOf(data, newSize);
    }
    //获得data数组的最大容量
    public int getCapacity() {
        return data.length;
    }
    //获取顺序表已经存放数据的数量,同length()
    public int size() {
        return length;
    }
    //取得顺序表下标为i的元素值,即data[i]
    public T get(int i) {
        if(i<0 || i>length-1)
            return null;
        return (T)data[i];
    }
    //修改下标值为i的元素值,即修改data[i]
    public boolean set(int i,T x) {
        if(i<0 || i>length-1)
            return false;
        else{
            data[i]=x;
            return true;
        }
    }
    //内部使用,提供数据元素的比较方法
    private int compare(T a,T b) {
        if(a instanceof Comparable && b instanceof Comparable) {
            return ((Comparable) a).compareTo((Comparable) b);
        }
        else {
            return ((String) a).compareTo((String) b);
        }
    }
    //在begin和end之间查找值为value的数据元素下标,使用顺序查找法
    public int indexOf(int begin,int end,T value) {
        //先查找null值
        if(value==null) {
            for(int i=0;i<length;i++) {
                if(data[i]==null) {
                    return i;
                }
            }
        }
        //查找有效值
        else {
            for(int i=0;i<length;i++) {
                if(compare((T)data[i],value)==0) {
                    return i;
                }
            }
        }
        return -1;
    }
    //内部使用,自动增加顺序表容量
    private void grow() {
        int newSize=data.length+incrementSize;
        data=Arrays.copyOf(data, newSize);
    }
    //在i位置插入数据元素x
    public void add(int i, T x) {
        //顺序表存满的时候自动增加顺序表容量
        if(length==data.length)
            grow();
        if(i<0) i=0;
        if(i>length) {
            i=length;
        }
        for(int j=length-1;j>=i;j--) {
            data[j+1]=data[j];
        }
        data[i]=x;
        length++;
    }
    //向表尾添加数据元素x
    public void add(T x) {
        if(length==data.length) {
            grow();
        }
        data[length]=x;
        length++;
    }
    //向表尾添加数据元素x,功能同上
    public void append(T x) {
        add(x);
    }
    //内部使用,以有序方式插入数据元素x
    private void insertOrder(int end,T x) {
        if(length==data.length) {
            grow();
        }
        int k;
        for(k=end-1;k>=0;k--) {
            //当x小于data[k]时
            if(compare(x,(T)data[k])<0) {
                data[k+1]=data[k];
            }
            else
                break;
        }
        data[k+1]=x;
    }
    //以有序方式向顺序表中插入数据元素x
    public void addSort(T x) {
        insertOrder(length,x);
        length++;
    }
    //对顺序表排序
    public void sort() {
        for(int i=1;i<=length-1;i++) {
            //调用插入函数insertOrder完成具体插入过程
            insertOrder(i, (T)data[i]);
        }
    }
    //删除下标为i的元素,并返回被删除的元素
    public T remove(int i) {
        if(i<0 || i>length-1)
            throw new IndexOutOfBoundsException("下标越界 i="+i);
        T olddata=(T)data[i];
        for(int j=i;j<length-1;j++) {
            data[j]=data[j+1];
        }
            length--;
            data[length]=null;
            return olddata;
    }
    //删除值为value的元素
    public T remove(T value) {
        int i=indexOf(value);
        return remove(i);
    }
    //清除整个列表
    public void clear() {
        for(int i=0;i<length;i++)
            //将元素指向空指针,使用Java虚拟机的辣鸡回收机制自动处理
            data[i]=null;
        length=0;
    }
    //将顺序表转化成字符串,便于直接输出顺序表
    public String toString() {
        StringBuilder strbui=new StringBuilder();
        strbui=strbui.append("(");
        for(int i=0;i<length-1;i++) {
            strbui.append(data[i].toString()+",");
        }
        strbui=strbui.append(data[length-1].toString()+")");
        String string=new String(strbui);
        strbui=null;
        return string;
    }
    //将顺序表转化成Object数组,并返回
    public Object[] toArray() {
        return Arrays.copyOf(this.data, this.length);
    }
    //将顺序表转换为类型为T的数组(因为T是泛型,理论上可以代表任何变量类型)
    public T[] toArray(T[] a) {
        //当传入的数组a的长度小于顺序表的长度时,创建一个与数组a的运行时类型相同的数组,a的长度可以是0
        if(a.length<length) {
            return (T[]) Arrays.copyOf(this.data, this.length,a.getClass());
        }
        //将data里面的以第1个元素复制到a中第1个元素,以此往后加一,一共加到this.length-1的位置
        System.arraycopy(this.data, 0, a, 0, this.length);
        if(a.length>this.length) {
            a[length]=null;
        }
        return a;
    }
    //创建一个迭代器类
    class MyIterator implements Iterator<T>{
        private int index=0;
         
        public boolean hasNext() {
            return index!=length();
        }
        public T next() {
            //用索引来获取SeqList中下标为index的项
            return get(index++);
        }
        public void remove() {
            //未实现此方法
        }
    }
    //返回一个迭代器对象
    public Iterator<T> iterator(){
        return new MyIterator();
    }
}

 

使用顺序表示例

创建一个顺序表并对以上类中函数进行实现检验,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*
 * created on April 16 21:33 2019
 *
 * @author:lhy
 */
package DS;
import java.util.Iterator;
 
import DS.SeqList;
 
/*
 * SeqList API介绍
 * setIncr(int inc): 设置顺序表每次增加时增量的大小
 * setCapacity(int newSize): 设置新的最大容量
 * getCapacity(): 获取数组的最大容量
 * size(): 获取顺序表已经存放数据的数量,同length()
 * get(int i): 获取顺序表下标为i的元素值
 * set(int i,T x): 修改下标值为i的元素值,即为修改data[i]
 * indexOf(int begin,int end,T value): 在begin和end之间查找值为value的元素下标值,使用顺序查找法
 * indexOf(T o): 在整个线性表中查找元素o的下标值
 * indexOf(int begin,T o): 在下标为begin到末尾的线性表中查找元素o的下标值
 * add(int i,T x): 在i位置插入数据元素x
 * add(T x): 在表尾添加数据元素x
 * append(T x): 在表尾添加数据元素x
 * addSort(T x): 以有序方式向顺序表中插入数据元素x
 * sort(): 对顺序表从小到大排序
 * remove(int i): 删除下标为i的元素,并返回被删除的元素
 * remove(T value): 删除值为value的元素,并返回被删除的元素
 * clear(): 清除整个顺序表
 * toString(): 将顺序表转化成字符串,便于直接输出顺序表
 * toArray(): 将顺序表转化成Object数组,并返回
 * toArray(T[] a): 将顺序表转换为类型为E的数组
 * iterator(): 返回一个迭代对象
 * length(): 获取线性表长度
 * isEmpty(): 判断线性表是否为空
 */
 
public class Test_SeqList {
 
    public static void main(String[] args) {
        //new一个顺序表对象
        SeqList<Integer> seqList= new SeqList<Integer>(16);
        System.out.println("顺序表是否为空:"+seqList.isEmpty());
        //随机初始化顺序表
        for(int i=0;i<6;i++)
        seqList.add((int)(1+Math.random()*(1000)));
        //创建一个顺序表迭代器,并输出顺序表
        Iterator<Integer> seq_iterator=seqList.iterator();
        System.out.print("使用迭代器输出整个顺序表:");
        for(int i=0;i<6;i++)
            System.out.print(seq_iterator.next()+" ");
        System.out.print("\n");
        System.out.println("初始化后,顺序表是否为空:"+seqList.isEmpty());
        System.out.println("获取顺序表容量:"+seqList.getCapacity());
        System.out.println("删除下标为2的元素:"+seqList.remove(2));
        System.out.println("删除下标为2的元素之后顺序表为:"+seqList.toString());
        seqList.append(78);
        System.out.println("顺序表末尾加入数据78之后顺序表为:"+seqList.toString());
        seqList.sort();
        System.out.println("将顺序表从大到小排序后顺序表为:"+seqList.toString());
        seqList.add(1,233);
        System.out.println("在下标为1的位置插入元素233之后,顺序表为:"+seqList.toString());
        System.out.println("获取下标为length-1(最后一个元素)的位置的元素为:"+seqList.get(seqList.length()-1));
        seqList.add(998);
        System.out.println("顺序表末尾加入数据998之后顺序表为:"+seqList.toString());
        seqList.sort();
        seqList.addSort(555);
        System.out.println("对顺序表排序后,有序插入数据555顺序表为:"+seqList.toString());
        System.out.println("查找排序后的元素555所在的位置:"+seqList.indexOf(0,seqList.length(),555));
        seqList.setCapacity(32);
        System.out.println("将顺序表最大容量变成32之后,最大容量为"+seqList.getCapacity());
        Integer num=new Integer(555);
        System.out.println("删除元素555:"+seqList.remove(num));
        System.out.println("删除元素555之后顺序表为:"+seqList.toString());
         
        //将seqList顺序表转化成Integer[]数组,使用toArray(),这个函数返回的是Object[]类型的数组
        Integer[] nums=new Integer[seqList.length()];
        //TIPS:因为Integer[]不是Object[]的子类,所以不能直接强制类型转换(Integer是Object的子类)
        Object[] nums_object=seqList.toArray();
        for(int i=0;i<nums.length;i++)
            nums[i]=(Integer)nums_object[i];
        System.out.print("输出转化后的数组num:");
        for(int j=0;j<nums.length;j++) {
            System.out.print(nums[j]+" ");
        }
        System.out.print("\n");
         
        //将seqList顺序表转化成Integer[]数组,使用toArray(T[] a)),这个函数返回的是T类型的数组
        Integer[] nums2=new Integer[seqList.length()];
        nums2=seqList.toArray(nums2);
        System.out.print("输出转化后的数组num2:");
        for(int j=0;j<nums2.length;j++) {
            System.out.print(nums2[j]+" ");
        }
         
        System.out.print("\n");
        System.out.println("使用clear函数清空整个顺序表");
        seqList.clear();
        System.out.println("顺序表现在是否为空:"+seqList.isEmpty());
    }
 
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
顺序表是否为空:true
使用迭代器输出整个顺序表:700 889 70 218 510 319
初始化后,顺序表是否为空:false
获取顺序表容量:16
删除下标为2的元素:70
删除下标为2的元素之后顺序表为:(700,889,218,510,319)
顺序表末尾加入数据78之后顺序表为:(700,889,218,510,319,78)
将顺序表从大到小排序后顺序表为:(78,218,319,510,700,889)
在下标为1的位置插入元素233之后,顺序表为:(78,233,218,319,510,700,889)
获取下标为length-1(最后一个元素)的位置的元素为:889
顺序表末尾加入数据998之后顺序表为:(78,233,218,319,510,700,889,998)
对顺序表排序后,有序插入数据555顺序表为:(78,218,233,319,510,555,700,889,998)
查找排序后的元素555所在的位置:5
将顺序表最大容量变成32之后,最大容量为32
删除元素555555
删除元素555之后顺序表为:(78,218,233,319,510,700,889,998)
输出转化后的数组num:78 218 233 319 510 700 889 998
输出转化后的数组num2:78 218 233 319 510 700 889 998
使用clear函数清空整个顺序表
顺序表现在是否为空:true
posted @ 2022-04-14 19:13  hanease  阅读(354)  评论(0编辑  收藏  举报