; L( G4 H% e4 n n8 [【Java演示】什么是链表?数据结构; ?, x% {! N/ |
一、单向链表
5 B" W; x; ]: |& h; t& |) P4 {4 A. Y9 | s
0 c1 x2 S# P% V+ N: Q# H链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。# C: ~6 O: j6 g) f3 G1 I# \) ^
单向 链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。! P3 J% {3 n' g5 j
链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指针指向空。/ Q* c- |, d% E6 F/ j" V; p! B+ X% c
# `' n* S, I: @6 u4 O什么叫随机存储呢?: s1 D1 J" T5 h! B
( p8 K8 D, {8 K9 c5 f" a如果说数组在内存中的存储方式是顺序存储,那么链表在内存中的存储方式则是随机存储 。( _, z v+ N, j( I
上一节我们讲解了数组的内存分配方式,数组在内存中占用了连续完整的存储空间。而链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。5 T. F; e3 m. G- c2 p
/ a d, m" L. i% v1 E1 z2 @ j
, v" w1 U) N7 W- L( s, h1 q图中的箭头代表链表节点的next指针。 链表的基本操作1. 查找节点在查找元素时,链表不像数组那样可以通过下标快速进行定位,只能从头节点开始向后一个一个节点逐一查找。
7 `8 @0 R% f9 P" ~' Z# R6 @
: N2 D/ F4 m0 |. _' a h3 i/**9 g- k" I# v8 @
* 链表查找元素
$ |* V7 s* T6 o& g2 K$ F; \ *" B7 N h2 \" a5 O# j
* @param index 查找的位置1 x- X/ i8 B1 E! K1 h
* @return index位置的Node对象$ r0 j4 M: G5 s1 s" n
*/- ^+ Q$ R2 a( K4 ~" v: z% ?
public Node get(int index) {
. J. v6 X' J2 A) Z4 Y if (index < 0 || index > size) {# H' D# ?8 t0 J5 Z6 H5 a: R
throw new IndexOutOfBoundsException("超出链表的节点的范围!");3 ?3 Y2 V+ q& {* w7 \$ B& E
}& ^+ Z( V" B* O' X- B' A
Node temp = head;
6 X' v% d4 @2 g for (int i = 0; i < index; i++) {& `8 P) F/ `) A& f) A7 s) f. K
temp = temp.next;+ s6 s( n4 j2 A0 l
}: H: l, y( {2 C) }
return temp;
/ H# z7 m* d+ }9 h9 l1 W }
7 y$ i. C0 J9 j* p( S5 v0 |; Q/ Y- j" z" _, k4 P5 |' `
链表中的数据只能按顺序进行访问,最坏的时间复杂度是O(n) 2. 更新节点
8 F, h; w+ T" o* D9 I
* ]! k$ F6 _ P& A" ~. b如果不考虑查找节点的过程,链表的更新过程会像数组那样简单,直接把旧数据替换成新数据即可。" l4 F! g W# I! t' h* ]# B; A' h& D
如果不考虑查找元素的过程,只考虑纯粹的更新节点操作,时间复杂度是O(1)( S$ A( r% ^, c# V5 }; L
/**5 B* ~. D7 a" u5 j) b
* 更新节点 将列表中指定位置的节点的data替换为指定的data。3 T4 }- d0 k. T+ I/ J
*9 e& [; ?; f" J2 A
* @param index 需要更新的节点的位置
9 F: d6 V$ T3 K9 @2 U * @param data 新data8 X+ L# _- X' M$ ]
* @return 旧data
- G! k! s/ N! k1 Q */
2 x$ j0 v* _4 q' g# @3 T public int set(int index, int data) {6 T2 S2 \3 X' Z8 J4 h, q
Node x = get(index);0 ?: [# R- G) ]
int oldVal = x.data;
1 Z( \' z; q2 d+ N: I% B* L0 F0 m x.data = data;+ q2 Z S- k0 t2 K, {# V3 a! `; r) \
return oldVal;) _" V/ c( @, {* ?3 O
}
: F2 `9 ~* z7 Q* x
- r( S$ X% w$ R1 A9 I3. 插入节点只要内存空间允许,能够插入链表的元素是无穷无尽的,不需要像数组那样考虑扩容的问题。 与数组类似,链表插入节点时,同样分为3种情况。 - 尾部插入
- 头部插入
- 中间插入/ x# ]' H- w+ V0 Y! Z
3.1. 尾部插入尾部插入,是最简单的情况,把最后一个节点的next指针指向新插入的节点即可。 ' O5 }& Y2 T& B4 e- O) K. v
7 M L2 I/ c$ ^7 k K2 t3 a3.2. 头部插入头部插入,可以分成两个步骤。 - 第1步,把新节点的next指针指向原先的头节点。
- 第2步,把新节点变为链表的头节点。7 h" i+ D9 q" o( M: R! _; b
$ {" r5 a5 P6 a, k
+ l9 x, D7 ^9 c& M0 m
3.3. 中间插入中间插入,同样分为两个步骤。 - 第1步,新节点的next指针,指向插入位置的节点。
- 第2步,插入位置前置节点的next指针,指向新节点。+ c( r& Y$ q: E0 N8 v7 h
1 L7 u4 N1 l" j& q9 r7 M
! Y* B3 P4 C6 [
三钟情况的代码合到一起
6 b0 j E, ]5 w. c, g$ z" `' A
+ ~$ z( X$ H0 g/**; U9 f5 u" B X& }/ M
* 链表插入元素8 P# P- v0 p5 ^& E
*
# \6 V1 P ?. |" h" O' b" I * @param index 插入位置
- A; i+ Z$ r) I' V * @param data 插入元素 被插入的链表节点的数据, O6 G! k! k7 E! H
*/
/ ?5 L0 Q! g& [) G public void insert(int index, int data) {
* }1 K) g/ X' s6 M$ C" g if (index < 0 || index > size) {
* F0 T. s0 R: ~( H: }0 n5 f, U throw new IndexOutOfBoundsException("超出链表节点范围!");9 t, o [+ p- y6 T2 Z# d
}) j2 j6 t1 L g* [+ M/ c) X9 I4 C
Node insertedNode = new Node(data);
$ H! f+ X* B+ b d1 u0 g if (size == 0) {
' \# U/ j! N# ], m7 Z! W! D6 g //空链表
/ ^. o. O4 g; Y head = insertedNode;) a7 E. W" @8 H3 s9 x
last = insertedNode;
& g4 q; U5 C/ C% K x7 B# \ } else if (index == 0) {1 E; d. J5 y: x* L0 }* K
//插入头部9 E' ` T5 ^1 _* j5 I
insertedNode.next = head;
m& Y0 T3 [& T- U2 a6 j head = insertedNode;
* ?" u- O4 f6 t } else if (size == index) {
: O; e0 v! z# }, m //插入尾部
) R2 S6 h7 N0 a" }8 Z2 @6 |$ B last.next = insertedNode;' s8 I$ V! F: i* ?. }
last = insertedNode;! f4 G$ }- R2 r! ]
} else { A0 i a) e6 b
//插入中间
9 \2 t) Z; h# z6 b% x) S Node prvNode = get(index - 1);4 O8 N" g" ~1 G3 \2 J7 b% t
insertedNode.next = prvNode.next;
/ z4 J: `3 Z5 m$ z8 v) W9 r1 \7 H- X prvNode.next = insertedNode;
) b" n2 [8 g# ] }# D( `$ s4 s. v, {( P7 X( Z: n' N
size++;
. u5 H- u. i1 p. Q* j) U7 r' g }
' R% N3 z' c" B5 ?: m0 q0 V& j3 @! \! d" L4 w
/**: F6 `6 ], A6 J7 M9 l \- L( Q
* 链表插入元素/ H6 o; q1 C& q e8 d4 ]
*# h1 c6 }! r$ a' q$ u; @
* @param index 插入位置
/ Y( C* g2 V) u" Y9 Q9 r * @param data 插入元素 被插入的链表节点的数据: G; G% t$ Y. O. {9 B* q3 G7 T
*/" [. F0 b+ d& G P) e1 z
public void insert(int index, int data) {
4 Q" m! Z, A" ~, ?$ y( U; w9 w if (index < 0 || index > size) {( Q3 e- k' {1 O
throw new IndexOutOfBoundsException("超出链表节点范围!");
; @) s" b* Z/ v* R }! @8 `/ w" _$ T5 F7 O
Node insertedNode = new Node(data);; S+ Y, o9 z) k. c& h) X# W5 T
if (size == 0) {
D9 \) s* R v% I2 d' p0 z //空链表
G( b4 H$ S8 z2 R% t* `& t head = insertedNode; N# {- M, C' ~) R; [) h) k
last = insertedNode;
. i; l/ v% ?9 e" H } else if (index == 0) {5 }0 `/ v9 _8 [4 D1 \
//插入头部( t: S/ Z2 U( y* `% H
insertedNode.next = head;
7 K/ }% U1 Y2 }( L" } head = insertedNode;$ z/ H! m: p! f. I) z/ T
} else if (size == index) {) \( k, [+ Y- y
//插入尾部
% @ `% \) }( ?1 A6 X( [ last.next = insertedNode;2 t9 U# H7 o' @; d
last = insertedNode;
! o; K4 ~( F2 S' ~+ e } else {4 N' D% G, @4 q9 O5 A) @% G
//插入中间8 [2 S' I8 V$ y
Node prvNode = get(index - 1);
; I2 R+ X& ~, c% _9 O insertedNode.next = prvNode.next;
& G" Q. n' _ s/ Q0 O5 g' C prvNode.next = insertedNode;
; t5 y* F0 i! S) \ }
3 b3 w1 E7 j) ^% a. Q* ^3 X3 A size++;3 [4 `/ m7 b& S6 b4 |
}; J* o* z1 m D5 r5 ?# H% x& h6 ^
4. 删除元素链表的删除操作同样分为3种情况。 - 尾部删除
- 头部删除
- 中间删除
/ i8 T5 A" U8 ^5 Z, b, I# k1 m 4.1. 尾部删除尾部删除,是最简单的情况,把倒数第2个节点的next指针指向空即% Y' z1 `" J8 p
可。
* I# f& V8 R; b! m, u- M# u' }/ O& I( u
: _2 V, r! u% o( `! Y4 x% `5 z
4.1. 头部删除头部删除,也很简单,把链表的头节点设为原先头节点的next指针即可。
' K! u2 Q" i- m# ?, O# l+ P( {; A% L7 p( l. d- }
4.1. 中间删除中间删除,同样很简单,把要删除节点的前置节点的next指针,指向要' ` d9 H( X5 R6 V+ h6 y. Q
删除元素的下一个节点即可。
- v5 C% f/ M# l* r! c" M- ^1 O( n2 _ ~; x4 Q7 \ p1 A; h
9 t& b+ n. Y) J Y! z- |: S
这里需要注意的是,许多高级语言,如Java,拥有自动化的垃圾回收机制,所以我们不用刻意去释放被删除的节点,只要没有外部引用指向它们,被删除的节点会被自动回收。- F) H5 V, U* _
如果不考虑插入、删除操作之前查找元素的过程,只考虑纯粹的插入和删除操作,时间复杂度都是O(1)' ^3 q' I8 r2 r' l: w4 t
/**
" t* b0 v4 I0 p8 k * 链表删除元素
) `) l, d$ n7 @& U/ G4 ? *
m# m! Q* V3 T. e5 U * @param index 删除的位置
' C4 A3 s5 D; m1 \2 b7 D * @return 被删除的节点8 \) @+ j6 g4 v- m) { J
*/
4 R" Y M1 g2 z. i9 ^( l% K public Node remove(int index) {6 w" q4 n, v7 b' T* a! f
if (index < 0 || index > size) {
) m8 a; y' w% K- ` throw new IndexOutOfBoundsException("超出链表节点范围");
4 ?9 _! J$ E0 U( F6 g, { }
/ D+ b/ ]8 T7 t! _3 W Node removeNode;
4 }% P( H) y2 G) s) q1 q% z if (index == 0) {
3 Z; I3 e& y. _5 ~8 z if (size == 0) {
! b. l! N8 K+ B R$ ~, ~ throw new NullPointerException("当前链表为空,不可以进行删除操作");8 R2 a( l R4 p* d! O! B8 P
}
" C" Y0 {0 v" A //删除头节点3 I: `( ]& {" g
removeNode = head;
+ v8 m& A( a# F% Q" a$ z- j head = head.next;; f% z) G' |7 N) q/ X
} else if (index == size - 1) {
. H) @+ H/ {3 u: p8 x0 ^ //删除尾节点9 h( k3 r# C& g3 x$ @0 s3 _
Node preNode = get(index - 1);
9 n2 u$ f$ C, g; e( `; C9 K S K removeNode = preNode.next;
* |! k3 s. }, }4 W {* W* C preNode.next = null;& \. y- H$ J! W9 a8 f
last = preNode;: L# l) }( i) J
} else {$ b, Q5 x g/ n+ ~/ }
//删除中间节点
1 R# c: @/ t8 K Node prevNode = get(index - 1);
0 ^ W" Q, X" v& X removeNode = prevNode.next;
6 P6 H1 K/ `9 R" r) D prevNode.next = prevNode.next.next;
' P% q9 z X- ^ _. b, M" k+ C }, y; L* M! ?5 |. J X3 `, l
size--;
; U6 Y. p/ a Q2 D) K+ s* R+ Y& q( S. u return removeNode;
5 a7 F. G9 q2 U8 Z c }
1 Q0 I) W% M1 f2 L4 ]# `. X/ JJava实现链表的完整代码package chapter2.part2;
1 z& B, O" |4 c6 D( W8 |
, r8 f' p# N: i6 M" e( `0 i! h/**
. W( O$ `5 q7 [5 k+ j8 O * Created by IntelliJ IDEA.
/ `; f% E5 X4 n/ P *- O, ] p( ?" k) @
* @Author: 张志浩 Zhang Zhihao4 E) y; B- F. u) i9 _, u
* @Email: 3382885270@qq.com8 k; W. ?9 r' @7 ~! w& |" B
* @Date: 2020/5/3
0 h$ g6 i1 E" {* v9 G) E# p * @Time: 13:39
& v( _$ ~' r1 q+ Q9 L2 I * @Version: 1.0
: s& n/ a; Y: O3 n7 X */
& y* {; {2 d& l9 Z: Hpublic class MyLinkedList2 {. I0 J8 {# [) {! X9 n: H6 D
private Node head; //头节点8 u; V* V" r2 \/ x
private Node last; //尾节点
X/ m3 K3 g" E' H; C private int size; //链表实际长度
I0 Q7 U8 }- Y: i5 c+ [# C$ S L+ u9 v
public static void main(String[] args) {
$ |" H/ C+ a* U, X" \0 z MyLinkedList2 myLinkedList = new MyLinkedList2();
% F1 D" e- A3 S- a4 ~/ K1 E3 Z// myLinkedList.remove(0); // java.lang.NullPointerException: 当前链表为空,不可以进行删除操作
2 l* n1 W% U9 ~% N// myLinkedList.remove(3); // java.lang.IndexOutOfBoundsException: 超出链表节点范围& B+ p; _0 t1 j2 z$ [' T3 ?+ ^
myLinkedList.insert(0, 3);1 |; ?& Z4 ?9 R L9 E
myLinkedList.insert(1, 7);
, H; F- C8 M: U6 E, | myLinkedList.insert(2, 9);- I, }+ Q& }$ i) C+ y4 j( f
myLinkedList.insert(3, 5);, r8 m6 u4 f `+ }* K
myLinkedList.insert(1, 6);
3 s* b9 m% L( y5 A% [/ | myLinkedList.remove(0);
6 r2 ^, R2 o( |6 x myLinkedList.set(0, 23);7 y9 }( O6 J) z& v" y+ K
myLinkedList.output();
2 }8 O2 W: w1 F+ U9 c }
5 |: p. c! T& W: b$ A8 S }
7 Z) |7 k2 D9 E /**4 Q" [7 A U* g/ G5 E
* 链表插入元素9 R$ V1 p1 D0 e+ |3 G9 m6 Q% X
*
/ f, ^/ q ?0 E' ]0 V * @param index 插入位置
. A5 X/ P) p& S& C * @param data 插入元素 被插入的链表节点的数据- U/ G* `* P* W2 |6 C7 l
*/
k4 h6 w0 z- O8 d+ k7 b* @ public void insert(int index, int data) {
8 y6 z, P7 r* `, J! _ if (index < 0 || index > size) {
& i8 G9 h4 W4 ^3 ^9 n& l( f throw new IndexOutOfBoundsException("超出链表节点范围!");- ?9 K' V& T% { N" f+ `5 B1 p
}7 a/ u2 u% |2 F& R Z8 Q% D" R
Node insertedNode = new Node(data);/ O2 e X$ E1 J4 l
if (size == 0) {( m' `, h- g$ v0 V$ v8 i) j6 a! b
//空链表" q5 U$ t+ o7 E" G/ P
head = insertedNode;
5 j$ _/ Q. P$ E last = insertedNode;3 ^" k( X) a: W; Q& ~0 u0 U3 Y
} else if (index == 0) {* _( `! ^4 j$ |- p; m
//插入头部; n: z9 b, U. F/ y
insertedNode.next = head;; l+ B p" {' ~. l; R
head = insertedNode;
8 @! r2 x0 v D! Y& c } else if (size == index) {5 {* W% F- j9 p& T2 b
//插入尾部
& v3 K6 \5 o& k( W/ Y% ^! Y last.next = insertedNode;) U8 S# ~$ | C- [$ k3 H: f
last = insertedNode;
$ c+ }# F0 R- P( M1 T7 k } else {! V* t0 e# m2 r; l; X
//插入中间! O3 x, X8 ^8 ^: G
Node prvNode = get(index - 1);
( l% V4 H+ f7 q; d2 Y& S insertedNode.next = prvNode.next;
% X$ L' l/ ?! h7 F, Y3 U prvNode.next = insertedNode;
+ i+ }9 E% Z& y2 `9 f }1 |! \1 ^. l. `; m
size++;
: \% S- h- T/ K2 t5 S0 f4 W5 s } ^5 p6 E) Q+ B4 y* z
5 [6 ]8 Q# c; }! _
/**
, c d; h2 p& k* R * 链表删除元素, b( }+ h6 B L5 q7 q; g V+ I
*) T# z9 K& x8 i, E7 _% I8 A
* @param index 删除的位置
y M* z- }$ O4 |3 M * @return 被删除的节点2 U; N; s+ ^& q
*/( S5 S. t, u- m
public Node remove(int index) {
3 W; C1 }! B8 ? if (index < 0 || index > size) {
+ M( V5 R: v6 u. @9 E Q throw new IndexOutOfBoundsException("超出链表节点范围");2 I; d# F) y/ V- T& l
}5 Z! M. f* ]9 u! @: L: C
Node removeNode;4 m+ n! @4 p. Q- d" q
if (index == 0) {( S+ M! b) r1 X2 {' B
if (size == 0) {
0 a" Q6 E F" p1 T: p! C. h5 B throw new NullPointerException("当前链表为空,不可以进行删除操作");* Y0 o: g y+ U9 e3 K+ b! A
}
% R% c1 u9 J6 K: t+ q //删除头节点
o C4 V# o1 @* L, T* ? removeNode = head;) J1 k4 a# W4 {- B( z- N6 w
head = head.next;" z! q$ q" \# o* C. g
} else if (index == size - 1) {
9 i7 L! K- h A" N ] //删除尾节点% F, s4 n7 O# D4 ?* a
Node preNode = get(index - 1);
( R T5 J$ }7 H: M1 \8 ?4 d6 K removeNode = preNode.next;; ` z$ [! O4 }: @ N
preNode.next = null;9 M5 S* \) j5 P5 N
last = preNode;$ e" I" a$ C2 ^/ f) a& f
} else {+ M3 T9 r% [( r+ U) m5 v5 t7 k
//删除中间节点
; j: Y0 ?. b0 V9 O Node prevNode = get(index - 1);
: ]* B( @3 n% w, s removeNode = prevNode.next;
& l- U2 o) V% S/ g. o G prevNode.next = prevNode.next.next;
! l1 W' @; U5 W0 t }* H- B/ m2 `* \' R$ h% [5 p4 V6 }
size--;$ p* q6 b2 N9 d; d: a" N- X
return removeNode;% t+ w" j% s- T/ L2 }7 X
}5 K6 E/ C8 `- V/ r
4 m( A( G+ p! F+ h# r9 r, d
/**1 |: F6 M, j* c: ~- h1 Q
* 更新节点 将列表中指定位置的节点的data替换为指定的data。: {% k2 ` d- S: L
*
' `: X7 y u5 Q+ K * @param index 需要更新的节点的位置- B/ ^/ Z |9 Y1 c
* @param data 新data6 }# R/ H; f8 N( @& Z6 {
* @return 旧data
2 t6 M# o! x4 C' x. { */
, x( u v% o# k m! ^( I' b public int set(int index, int data) {5 H* k: f3 [6 o" ]% o1 e5 A
Node x = get(index);/ Z4 v6 R/ U$ ?/ z& z/ q5 ^
int oldVal = x.data;
& k, S( Q6 s% }0 B# J: }) G x.data = data;, o0 I/ Q- `3 n: S% h, j7 `
return oldVal;
4 E I9 S0 n: \; h& @ }
5 |; H( {( C/ K0 f6 W
9 g0 q% [6 i4 ]3 l \ /**' r% O) e/ O3 a6 d' j% b
* 链表查找元素
7 y. G7 e# h2 t( W *
3 C+ E5 b! }# f! y* D/ n$ d * @param index 查找的位置
% l8 J& E& V% C" F2 \. W * @return index位置的Node对象) g, X8 R3 g4 y+ p
*// _. M: G; P& n( I$ Q
public Node get(int index) {' t( G& \/ x) |; v4 B- h& o
if (index < 0 || index > size) {
5 m3 U2 L. x; s Q( {1 p- v+ F throw new IndexOutOfBoundsException("超出链表的节点的范围!");
& v7 `# R1 y f+ m. Y }' w2 x j. F2 Q- b# j
Node temp = head;
& S, |; x& h! T) i9 h! u/ E. ^4 K; i for (int i = 0; i < index; i++) {
' `* _ S3 F* E8 d temp = temp.next;" x$ }$ q9 P7 C8 y8 Y) _
}
6 C% M9 I' l/ ~ G return temp;1 ]$ F0 Y }! S$ k6 r4 r7 O
}6 h! b$ v) p- @. h( ^
, w4 G( @$ |' C1 ^+ K
/**
1 h: n0 q) V7 ?! i) [' t * 输出链表; J; H S, B! E7 X& d
*/3 z. I3 d% t+ w
public void output() {
; @0 f+ e0 w# | Node temp = head;
% q# R9 l: y* e4 y2 H. W1 i& {6 K while (temp != null) {$ U% D2 h* _* Q
System.out.print(temp.data + " ");
& _6 q3 a. d8 N' g& c9 c4 u temp = temp.next;
& e* h/ O$ U0 i2 u. } }
' A% e" R7 o9 o+ z% W }
5 F9 p. K' k ]( b$ J1 n% a
4 W7 F9 \: x1 | /**. H/ f8 h3 G2 y( v* s H
* 链表节点- D% O- K5 ]: O
*/
4 c5 U9 {6 M* @3 j1 Z5 } class Node {9 w- _/ z) M4 c' f. t7 c8 {
int data;
0 M! V2 |& K/ u" P' w, o Node next;& i# ]0 U, {9 a/ k U% T
1 T4 i% Y) h# D( M0 M. l6 d* A
Node(int data) {8 |1 Y3 o- u7 |% k8 ^/ M
this.data = data;
- d! d7 }* U+ s" i Y- V, W/ m. L* \ }+ i- r% F/ K$ ^7 p
}; o! A4 o9 J0 T' H# a
}( F9 g4 |$ X' {) @ Y- r
@$ f, h$ D5 l7 D/ l2 Q' p! @6 h J) p9 ?9 d7 z, ^) {
二、双向链表
; \/ G+ m: t- g p2 B双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev 指针。
7 q' P& l' e; Y, |
$ q, ~% [/ T- R( j
8 l* F& ~5 Q8 }% M3 s5 ~
4 m8 y7 y4 V [: p
4 f3 i5 O! E4 L4 |————————————————
( ^, {2 W% g! \: L' J# h1 v% ?% M版权声明:本文为CSDN博主「爱做梦的鱼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
H' Y7 C6 M s3 q3 i原文链接:https://blog.csdn.net/weixin_43124279/article/details/105904468
9 l7 M3 p x0 n# r u8 [+ U
s, h7 K6 s$ |9 A& w/ Y% [% b" T2 `
; q" A: M! P: l8 u/ w+ U: c7 _ |