9 E" |4 f' q6 m4 K5 ]【Java演示】什么是链表?数据结构
- |5 w5 B$ ?9 z6 J; I0 s/ C一、单向链表* X0 o7 U5 u+ f( B
) I- ~% A% [3 [* y: \+ t" `, s
" r. J: |6 J+ R+ Z5 ]6 K2 W
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。
8 Q9 F. C( x- c* y" W/ ~' [单向 链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。9 `9 r" p5 f. J" B: \$ J
链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指针指向空。
( l x0 Z4 F2 t8 ]9 d! W- o3 f# d& f) r6 A
什么叫随机存储呢?
5 g; z& C! o) [ Q7 A$ k) ^: W0 w& k9 F6 U- F# M: F
如果说数组在内存中的存储方式是顺序存储,那么链表在内存中的存储方式则是随机存储 。3 a. G" s* b- s5 ~8 {; }
上一节我们讲解了数组的内存分配方式,数组在内存中占用了连续完整的存储空间。而链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。 w! _% u& p5 e# t6 t
7 _; }6 m$ Q6 ^( a8 g+ `* ^1 `9 \8 y/ K* d. c: E$ V/ N4 T+ s
图中的箭头代表链表节点的next指针。 链表的基本操作1. 查找节点在查找元素时,链表不像数组那样可以通过下标快速进行定位,只能从头节点开始向后一个一个节点逐一查找。
# p0 s8 Q% Y- r- B" w1 a5 A; B- W6 `* @! L: g
/**
$ q0 o5 d6 b, l* y * 链表查找元素5 Y& P: z8 p" C4 T. @' P3 D% f
*( S8 a; U1 a# H$ b$ d: E$ r4 \
* @param index 查找的位置 T5 P/ F5 y+ E2 p3 x
* @return index位置的Node对象
/ t2 A. H9 c E; M; D* w9 }' U! { */
) Z) D: D# L7 O1 Q5 ~3 O# J. | public Node get(int index) {
5 B& c; X5 S$ G if (index < 0 || index > size) {! e- N1 `5 q& a/ R
throw new IndexOutOfBoundsException("超出链表的节点的范围!");8 B& `" e5 S7 w- Z, O
}
/ T$ R: J7 I7 A1 S; _# q3 @ Node temp = head;2 W$ x$ @2 L" [
for (int i = 0; i < index; i++) {% c2 o1 e4 j: {2 @" _8 r$ H
temp = temp.next;4 G6 o! z- b. z; t7 S: \( I$ p
}
$ s! y3 |- _; ^ W7 E2 M9 G! A return temp;" k6 x1 i: j2 d9 [2 {8 t# [
}
: n1 V/ V% S% |9 l+ \
* Z- B# G% k D5 [( ^: S; j3 @链表中的数据只能按顺序进行访问,最坏的时间复杂度是O(n) 2. 更新节点
8 K. P, p7 U; A9 z
5 n) `3 A( d+ ]' R$ S
如果不考虑查找节点的过程,链表的更新过程会像数组那样简单,直接把旧数据替换成新数据即可。' r+ E- H H; ^# d
如果不考虑查找元素的过程,只考虑纯粹的更新节点操作,时间复杂度是O(1)& ^( w1 ^: _8 Y
/**
/ k E# T7 q0 S * 更新节点 将列表中指定位置的节点的data替换为指定的data。6 k, r# l2 f m/ L) J
*
- J; H. f; Y& k* _& G" l% W * @param index 需要更新的节点的位置
3 q/ z0 n7 H2 i. x" y. ?( | * @param data 新data. L; f Z7 \, _# p! m
* @return 旧data, {& a% I4 s2 [+ `
*/4 o/ [$ w8 S) Y1 z5 M
public int set(int index, int data) {
) _4 x; K9 J) f Node x = get(index);
8 S9 B1 }1 n" F3 j& o1 T z int oldVal = x.data;
+ F8 H& m+ L; s x.data = data;& S1 f, H; s# v/ M1 u& m4 Q- K
return oldVal;
# T0 ?; W- P( j, R$ h% q/ k }
4 L( `% L' u; J2 n
" c: G6 S) |. w# w3. 插入节点只要内存空间允许,能够插入链表的元素是无穷无尽的,不需要像数组那样考虑扩容的问题。 与数组类似,链表插入节点时,同样分为3种情况。 - 尾部插入
- 头部插入
- 中间插入% p, n3 w$ V5 _# E( ~( c
3.1. 尾部插入尾部插入,是最简单的情况,把最后一个节点的next指针指向新插入的节点即可。 : T. C+ ?5 g6 Y1 P
& }" H7 S y% g" m' `1 m" ^3.2. 头部插入头部插入,可以分成两个步骤。 - 第1步,把新节点的next指针指向原先的头节点。
- 第2步,把新节点变为链表的头节点。- O, P. e1 }- m) X
" `4 s6 z5 R" Q' T% D; R
. _: |6 Z" @8 G) }$ x' H
3.3. 中间插入中间插入,同样分为两个步骤。 - 第1步,新节点的next指针,指向插入位置的节点。
- 第2步,插入位置前置节点的next指针,指向新节点。
6 q# t" f3 h9 y
2 s# i3 J9 j' _0 y8 T" X# H4 M
* J3 b: i& L+ j0 L4 j1 s三钟情况的代码合到一起
6 M7 E4 i, O# g" D. ]2 I. G# g) x- ?3 m [; G, [' P
/**
6 n, w5 Y: K* u; @8 f p' p, h * 链表插入元素: Q; Q' g% U% d$ n5 u
** l" I; Z( w1 I! v
* @param index 插入位置
" F9 }! {( F: C( F" L * @param data 插入元素 被插入的链表节点的数据
( P5 X9 W: k% ~9 u- d. z# p */1 z( h# U4 @! t M
public void insert(int index, int data) {
% i. z. D, h' Z# G if (index < 0 || index > size) {
( W( ]* U3 B$ D" O! Y throw new IndexOutOfBoundsException("超出链表节点范围!");
7 ]* [2 M2 d/ Q, _# @4 A+ P9 a& ^ }+ \# s9 B7 E1 {
Node insertedNode = new Node(data);
1 E8 _9 B3 K$ u. u1 j if (size == 0) {
5 n- k/ r8 t1 p2 H5 ? //空链表3 e, i$ X$ g9 Z' \2 Y" g$ k+ ?+ T
head = insertedNode;) n$ h9 h" ~7 `/ ?* s
last = insertedNode;
! O7 G$ L: B3 t i! \1 F5 r } else if (index == 0) {
3 J9 w) ]/ m, `3 b( v% S //插入头部, z m/ H6 ^7 j) N8 q4 V4 t0 V
insertedNode.next = head;
# l8 ^/ C- d: a t' t4 Y; v- M head = insertedNode;
8 g& B% l( F* } } else if (size == index) {: D9 E R9 b1 u, ]7 @3 z U3 g/ z8 R. O
//插入尾部) {: y0 W) o0 G3 Y/ P/ U( M
last.next = insertedNode;3 ]0 X- Z1 N/ q, f3 G% r: |
last = insertedNode;& P" y- v9 ^. b! Q0 p& x
} else {
! `$ Z# n' p$ l# Z) q) Q9 R //插入中间
0 C1 M! k& M0 N7 P Node prvNode = get(index - 1);
( e+ z% u( v. M' ]- g3 ?. K insertedNode.next = prvNode.next;
# p) J+ f) h0 t6 F9 E( y; ~, d, Q prvNode.next = insertedNode;
% H9 g6 N* A+ N8 E1 @; ^$ ] }9 e% u- Q% D* T. @
size++;
0 i+ P# C7 {9 v; }. T }
, w. D+ t0 T% i) `7 m- u
( v/ L# s% c: Y6 @) V G" b4 i$ g/**
4 G: P" [0 M3 g * 链表插入元素4 e% _" E9 \% W: \; n
*/ q( i, A4 M2 W
* @param index 插入位置8 W! ?) U+ `) S% w
* @param data 插入元素 被插入的链表节点的数据
7 J6 X# B2 }6 z1 \5 H& A9 F */
' h. }) w, b/ j6 u/ \* F public void insert(int index, int data) {+ N" b) } ~3 z2 g3 ]
if (index < 0 || index > size) {! ?* g2 o9 i5 c8 W$ z( s$ C. j$ M
throw new IndexOutOfBoundsException("超出链表节点范围!");
8 p) M h+ `4 y; R+ p3 [1 _ }3 G, k! s7 S: o" c2 z* y9 m
Node insertedNode = new Node(data);' }) q) O: |; X8 G' V
if (size == 0) {
: W/ H; P* j& Z& G' G1 E //空链表
/ D( `3 S! P" h; _' R head = insertedNode;
4 |1 |) D7 Q0 z+ j last = insertedNode; h) Y) p1 e1 ?% P- s
} else if (index == 0) {
3 L5 y4 U k7 K8 a9 T* [4 s+ y //插入头部
3 A% I, N) Q- J! H% o5 d insertedNode.next = head;% t. \0 F% @, b+ a! N5 t
head = insertedNode;2 K( E5 V B: j
} else if (size == index) {2 {* ?7 c( Z/ e
//插入尾部
+ q( P' N2 x) o& e6 b last.next = insertedNode;! P& H' |4 G9 N
last = insertedNode;& V& T' x% L. b3 _
} else {
# Q( r5 R1 S, {( R2 y //插入中间
2 A1 A2 {. }/ J& l- |; d3 N Node prvNode = get(index - 1);* J. g8 K1 ^" w8 `( |! g; e
insertedNode.next = prvNode.next;
( W% }: D& m5 x+ `9 p2 B prvNode.next = insertedNode;& \2 o) V7 g# ^; a1 e/ y: n4 z
}
3 Z, Z; ?0 H( s9 | \ size++;: N( u( o; R" W# Q1 ^* `" F
}
0 `- G; V7 y2 c/ }4. 删除元素链表的删除操作同样分为3种情况。 - 尾部删除
- 头部删除
- 中间删除# `+ ^! o7 j; v& [, {- v9 q8 s
4.1. 尾部删除尾部删除,是最简单的情况,把倒数第2个节点的next指针指向空即. h0 X1 v( t9 n+ N# ]
可。
' `; W, [; S8 w1 }! M) ^1 n/ m
: Q% \% s) @/ Q6 c7 V9 ]$ @: g% B4.1. 头部删除头部删除,也很简单,把链表的头节点设为原先头节点的next指针即可。
2 [/ v0 F- a* h( ?" o, Q% R( a
5 ]3 S+ b7 q+ T! N) K9 C* c: G4.1. 中间删除中间删除,同样很简单,把要删除节点的前置节点的next指针,指向要
3 l5 w& j% W. T: f删除元素的下一个节点即可。
, c( ?+ k7 P# K* S! B5 @
( l- g5 T* m. L+ l2 z" _& o0 z: a: R+ T. X$ Y/ |! @
这里需要注意的是,许多高级语言,如Java,拥有自动化的垃圾回收机制,所以我们不用刻意去释放被删除的节点,只要没有外部引用指向它们,被删除的节点会被自动回收。
( ~ M6 K) ~7 Q7 M; G; d; i e如果不考虑插入、删除操作之前查找元素的过程,只考虑纯粹的插入和删除操作,时间复杂度都是O(1)
% Y6 F4 _* P, ]6 r; J/**
8 l9 |" {' s# q" N5 O/ J * 链表删除元素
+ X( F D" k# S' K) S* M# S0 w *
2 S; b$ ~$ b; G7 X) o2 F1 v * @param index 删除的位置, `- V& b, }; B! h' Z" K0 X
* @return 被删除的节点
; k& J5 k% X0 Q/ x */0 w9 S Z6 d* l7 t; ?
public Node remove(int index) {
. g; g$ `2 N% c$ l4 d if (index < 0 || index > size) {/ A; G* r$ G# v) f4 ]5 O R0 \
throw new IndexOutOfBoundsException("超出链表节点范围");
; P7 B) P! j$ E }
* n5 `/ F1 N7 o& w Node removeNode;
b9 n. O3 t% @- O3 M if (index == 0) {3 I1 X3 p/ R& E
if (size == 0) {+ ?. ~* [ }" t) @! f
throw new NullPointerException("当前链表为空,不可以进行删除操作");- W: i M# H( C( K, ?
}/ N; E$ ]; b5 t$ L. A
//删除头节点
, f$ _! I) A9 u removeNode = head;% u0 O9 c3 C& F/ }7 G" J
head = head.next;
5 L$ r8 A. \4 y } else if (index == size - 1) {
( X2 w/ X* T% U: R //删除尾节点
8 O# ]7 O: y3 x7 c Node preNode = get(index - 1);4 H5 q( J" Y+ N3 l0 C/ @
removeNode = preNode.next;# K9 i. A* S% B# N+ o
preNode.next = null;
# z" u* b& Y: ` last = preNode;
# f' z$ n: n0 C) f, f } else {" c4 E0 e# K" V0 R5 t
//删除中间节点. B+ s' g+ I9 H# P% a
Node prevNode = get(index - 1);
( j& `" ~5 ?$ B; N" f) D# w- G( w removeNode = prevNode.next;
0 E" n+ n0 n1 M1 c# x2 k4 o: g( \ prevNode.next = prevNode.next.next;
, |) s6 s$ Y3 y! \) b7 Z& t% [ }* N' I* c* C% T
size--;7 p) B) e# ]: r/ W
return removeNode;
7 X# O, R) ]1 _; c }! G" Z/ n# R' k
Java实现链表的完整代码package chapter2.part2;7 O; W/ d- K8 }: B
. B) F% w, x1 G( f& a% o
/**
0 N9 n, w* z* d# @: f * Created by IntelliJ IDEA.
3 O7 G' l4 s3 C- T; L *
: m8 A1 |+ H& e# s* Z' [ * @Author: 张志浩 Zhang Zhihao
% b5 K$ [7 g: _& v" ` * @Email: 3382885270@qq.com6 M$ N7 s5 M ^# s: ~8 f v# e
* @Date: 2020/5/38 p2 C" l! u& W. Z9 ]- e0 T% L
* @Time: 13:39
+ f& E9 H1 J! r, ~1 g * @Version: 1.0
$ e1 J% m/ x3 I */
% `; w; _6 p9 _5 H' }1 ]; H; U& wpublic class MyLinkedList2 {6 Z" |6 {: N$ [4 N: R
private Node head; //头节点
# a. `) @5 G; X) P- Y private Node last; //尾节点9 p( @; G& ^& |: d, o/ B2 q
private int size; //链表实际长度
% x5 I5 v. g; m. [" K3 `5 V$ ]9 ^
" g$ \1 a+ q1 u6 a$ I public static void main(String[] args) {& i# z7 @/ q) c% @7 |8 V
MyLinkedList2 myLinkedList = new MyLinkedList2();
; [ j* U6 o' s) Z. i. g& a8 |// myLinkedList.remove(0); // java.lang.NullPointerException: 当前链表为空,不可以进行删除操作
" M$ ^2 ]0 t9 S0 H$ \% q// myLinkedList.remove(3); // java.lang.IndexOutOfBoundsException: 超出链表节点范围 g# M0 K3 v% i7 S8 s4 y9 g
myLinkedList.insert(0, 3);
! e0 ^& w6 R# p0 `$ [5 r" O% M! ~ myLinkedList.insert(1, 7);
! L6 ]% \3 H# r$ {6 j myLinkedList.insert(2, 9);. \* Y- |; x0 e* u: L& ]. f
myLinkedList.insert(3, 5);
: y4 |- G& Q& z myLinkedList.insert(1, 6);7 G. I$ v5 X3 e6 Z* Q, L/ y
myLinkedList.remove(0);( l% n3 \$ b" E" ~
myLinkedList.set(0, 23);
, q' S. M( h! D myLinkedList.output();
; P3 G6 c2 C; W9 E }
# [$ r" N. f# ?/ P2 S9 V5 u$ L9 D7 z- w% `6 C" G4 Y* Q
/**: B, \5 ]5 s' ]5 M% B/ G7 }
* 链表插入元素
$ }6 Z/ p J/ j *8 s1 J9 W( z/ y- T/ a; S6 G- ~: q
* @param index 插入位置% G8 x3 h/ q0 b7 i b
* @param data 插入元素 被插入的链表节点的数据
; J, C6 M: n7 W5 j* B */
3 o/ Q! W( l8 S, N' P7 g public void insert(int index, int data) {
% E0 }5 p2 O! ]7 X0 p5 o2 o8 k if (index < 0 || index > size) {( x3 F$ \/ O! q- e* m
throw new IndexOutOfBoundsException("超出链表节点范围!");& v4 A9 h, J3 b
}) n7 c, J. I( }: K! L4 F$ y! R
Node insertedNode = new Node(data);
0 \# s9 Y+ g+ v if (size == 0) {
% g( i4 P F2 K' g //空链表
0 t2 E& x: g; I9 a; U- W head = insertedNode;
- g% ]+ \) y7 A last = insertedNode;$ N5 u% ]- [9 _" l& r
} else if (index == 0) {
2 o$ n) J) j% l: C( H) K //插入头部* x0 B; G- W3 V- j1 _! h3 }" [4 j
insertedNode.next = head;
1 w& t& v2 U7 I2 a) w; u! g head = insertedNode;' K; ]" T9 g8 e0 @
} else if (size == index) {' F: G* [. }5 P# L( H
//插入尾部+ U' b# d! E) f7 Q2 d7 ~$ @# @
last.next = insertedNode;
/ E, c1 X$ y9 U last = insertedNode;# U+ t+ H9 A5 m! g# y! H4 |
} else {2 U& f7 k1 \3 U& q. [. [
//插入中间1 G# f2 e4 \: f D8 X
Node prvNode = get(index - 1);
+ M5 b- r7 w% {& Q% S7 X9 I1 K insertedNode.next = prvNode.next;
& y0 w+ N1 j# r prvNode.next = insertedNode;8 B! P2 j- ? A* A
}3 z/ ^4 }/ U4 f" l- s) m; y, U
size++;
; r8 ]$ p6 l, N6 ? }
% u! h) {$ k( ~
8 M: y7 q2 A$ [' D- @ /**
0 O6 P, Q- `2 u& x * 链表删除元素# K$ ?- W0 D' G ^
*
/ C/ i. h, I/ P6 H& h * @param index 删除的位置1 u5 O* _# v, u
* @return 被删除的节点
( E p0 s& ?/ Q+ C. { */
9 ~ S R5 O* u) { public Node remove(int index) {
5 t9 q9 o. |" s" i# b& A if (index < 0 || index > size) {
& }" U, I" x; ?, \) `# V( v throw new IndexOutOfBoundsException("超出链表节点范围");
1 Z R8 K/ y* u/ ?) k( {; r' } }% G4 |4 A- H" l0 T8 q3 l
Node removeNode;4 H: a' V/ t! s+ y# l2 K) K9 c
if (index == 0) {
+ v4 H! c3 I* B I2 u& p# j0 C if (size == 0) {
6 W2 I! ~8 M9 t9 F# f6 O) K( Q C throw new NullPointerException("当前链表为空,不可以进行删除操作");
3 \$ d4 ^; [6 Q) j( V- s }( q" v. V6 \1 g1 J
//删除头节点
( g) p1 H3 f7 F/ k7 L removeNode = head;1 F. F% m. Z2 Q) s
head = head.next;2 Q) P, u5 U5 \% v/ ?/ x
} else if (index == size - 1) {
/ r+ A) p! a! S$ V; g$ H2 `" Y //删除尾节点- h h8 n" B* Z$ b
Node preNode = get(index - 1);( Q* z' M: B2 {: J0 ~
removeNode = preNode.next;: q/ ^5 t# H! k: a; m
preNode.next = null;
?% |' H5 u0 f last = preNode;
) [- N2 J% }8 O- b1 I } else {
& F" d- R$ C: J //删除中间节点4 Y# _9 S& X j5 S3 o
Node prevNode = get(index - 1);
1 X& Z; q" L8 F9 \) ]- b4 a removeNode = prevNode.next;
0 t' i6 B" @6 @4 t/ B prevNode.next = prevNode.next.next;
0 @9 _5 Y8 Q9 M, P' B) ^ }8 Q1 v! `; v7 b q: ^0 }! U! b$ ?2 c) h
size--;8 y, x# b+ h5 l9 {5 d5 G* k5 \
return removeNode;
( `7 A9 y2 l, }0 I4 N }
s/ G! n' M! I# H2 |7 H
$ O! x) ?8 {# d8 A$ J; _ /**
1 w/ F; X2 d3 V# D: i4 O * 更新节点 将列表中指定位置的节点的data替换为指定的data。 r6 w2 |) X- {3 ?
*
% C* f; G* s) i, D * @param index 需要更新的节点的位置2 ~) u# x" e4 f" H- `. a ^6 m
* @param data 新data# M% v: a, a0 J: c' u( ]
* @return 旧data* B, A+ r3 _) _# j% k9 F
*/1 }! e7 a1 c. g, G1 v
public int set(int index, int data) {# w b3 m. F: A- I8 G
Node x = get(index);) [7 Z+ m- R9 r d
int oldVal = x.data;
1 q( j5 S" `( O5 @, d x.data = data;8 t# L# |& {$ c, k* I" S
return oldVal;
9 X2 r8 f9 k8 m7 d% O$ W# j3 _( D }
! }, l# x' k9 L8 o* g7 _: r" i& D* n. [! o
/**% \# u* k( K5 {: q
* 链表查找元素
, ]4 [& o# C- P, J/ ]# A *
* O) f/ ]) @5 S/ Q * @param index 查找的位置5 A2 X- J, r* p; }8 x7 L
* @return index位置的Node对象/ G& Q1 A/ B9 E1 P3 T2 N# }; x
*/1 K5 K- M$ l- O# J# t3 A3 H: O9 a8 l
public Node get(int index) {
- x4 M4 O8 ?* x if (index < 0 || index > size) {- \ _! E9 L \/ }! a: \5 x
throw new IndexOutOfBoundsException("超出链表的节点的范围!");+ f" H+ N$ K3 E, _
}6 b+ l5 h% `( e! p! v, E# J
Node temp = head;
: k1 F/ k0 o; i7 G$ z3 a+ K for (int i = 0; i < index; i++) {
: b6 o" M5 i0 h4 J, P! F temp = temp.next;4 {* H! |/ L1 b- P
}
9 W- d O4 M# m1 q return temp;
5 O3 C+ A0 b; f, ?& [3 B i' J }
6 `4 V; Z5 Z0 W+ h) a
+ l2 O. a g, t+ _$ V% N /**% w6 X6 L$ _* x
* 输出链表
1 J' M; \# `# A */6 N3 s5 J. E: C! U( L
public void output() {
8 g" F" G. e, `4 X# [$ J Node temp = head;
5 S% j0 L0 A5 }$ S7 _% B/ f while (temp != null) {$ M9 R4 }& k/ E/ F/ |
System.out.print(temp.data + " ");
4 A; k7 p( K4 y* N0 ^/ Z; s% K* @ temp = temp.next;
/ a2 d' k! ^7 B }
0 r/ [" o& a& Z x' M& ~ }
& r& p0 u( x3 T0 |; o1 |! G# G5 {4 q
/**
9 X- F" Y! u; d * 链表节点
- x/ @+ \9 M5 M6 Q */2 _/ t T" Q: W
class Node {
5 S& Y9 J( u6 O+ L F/ G3 D! e& c2 X$ p1 T int data;9 E* M2 N' \3 |9 a( }# m) f- n% h
Node next;1 q* I* K/ E7 S; l
; U6 A4 P' @6 r* h
Node(int data) {
8 P5 j( \$ ]+ r4 ?+ @3 _& s this.data = data;
4 \* J6 h8 f- [ }
9 B/ R4 h6 ]& `# d, C }
5 c) q7 N: C$ x9 H7 P}
9 |2 C' q- E/ f/ |- f, E7 d, | I' v d/ ]9 F' W
1 B5 g9 S7 A0 {9 k* d二、双向链表
! C3 o0 k( l" q6 ]& _ x双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev 指针。
6 v* s$ Y( m5 q$ y
- k+ ~5 V' R7 A" }7 l) R( y: ~. g# p
, g' b( l0 p3 E- Z0 u
$ c3 U! f' L$ x. Y8 e. I8 S————————————————/ Z2 {2 q& Z. ]: z+ R4 `
版权声明:本文为CSDN博主「爱做梦的鱼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
4 h/ ~3 g# H7 I) b原文链接:https://blog.csdn.net/weixin_43124279/article/details/1059044685 _" l; Q. o& z
2 s8 ?" `* m/ Q& h# H6 @: y/ e- i5 [
. b: j; L* N& w$ [# ]' d
|