|
| 1 | +# 自动动手系列——实现List |
| 2 | +该包代码 主要实现了ArrayList与LinkedList。该README对应介绍实现的内容与过程。下面分别是ArrayList与LinkedList实现 |
| 3 | +其ArrayList实现的内容和过程如下: |
| 4 | + |
1 | 5 | # 自己动手系列——实现一个简单的ArrayList |
2 | 6 | ArrayList是Java集合框架中一个经典的实现类。他比起常用的数组而言,明显的优点在于,可以随意的添加和删除元素而不需考虑数组的大小。处于练手的目的,实现一个简单的ArrayList,并且把实现的过程在此记录。 |
3 | 7 | 实现的ArrayList主要的功能如下: |
@@ -209,4 +213,281 @@ remove(Object o)和remove(int index) |
209 | 213 | ## 总结 |
210 | 214 | 自此,一个简单的ArrayList就实现完了,实现的目的是为了弄清ArrayList动态数组的原理以及add与remove方法的内容实现。同时,也清楚了ArrayList最大的扩容空间就是Integer的最大值。该类的所有代码在[SimpleArrayList代码](https://github.com/byhieg/JavaTutorial/tree/master/src/main/java/cn/byhieg/collectiontutorial/listtutorial) |
211 | 215 |
|
| 216 | +# 自己动手系列——实现一个简单的LinkedList |
| 217 | + |
| 218 | +LinkedList与ArrayList都是List接口的具体实现类。LinkedList与ArrayList在功能上也是大体一致,但是因为两者具体的实现方式不一致,所以在进行一些相同操作的时候,其效率也是有差别的。 |
| 219 | +对于抽象的数据结构——线性表而言,线性表分为两种,一种是顺序存储结构的顺序表,另一种是通过指针来描述其逻辑位置的链表。 |
| 220 | +针对于具体的Java实现: |
| 221 | + |
| 222 | +- 顺序存储的顺序表是用数组来实现的,以数组为基础进行封装各种操作而形成的List为ArrayList |
| 223 | +- 链表是用指针来描述其逻辑位置,在Java中以双向链表为基础进行封装各种操作而形成的List为LinkedList |
| 224 | + |
| 225 | +针对插入与删除操作,ArrayList每插入一个元素,首先需要判断数组的空间够不够,不够要进行扩容,在有足够的空间的基础上,在指定的index位置上插入元素,但是该index及以后的元素都要后移。虽然删除操作不需要判断空间够不够,但同样需要该index及以后的元素向前移动,这些移动的操作会增加时间的复杂度。但是对于LinkedList就不一样,因为使用指针来指示其逻辑的位置,所以插入与删除的操作的时间复杂度都是 ** O(1) ** |
| 226 | + |
| 227 | +虽然对于ArrayList而言,插入与删除的时间复杂度很高,但是对于查找指定位置的元素这种操作而言,就非常的快,因为可以通过数组直接得到该下标对应的元素。反而,LinkedList而言,无法直接返回指定位置的元素,需要一个个查询,其时间的复杂度就是 ** O(n) ** |
| 228 | + |
| 229 | +与实现[ArrayList教程](http://www.cnblogs.com/qifengshi/p/6377614.html)一样,实现的目的主要在于练手以及掌握官方实现的原理和一些技巧,因此很多需要与其他类配合的方法和功能,就先不在这里实现如`iterator`等 |
| 230 | + |
| 231 | +所以,实现的LinkedList的方法如下: |
| 232 | + |
| 233 | +- add方法 |
| 234 | +- get方法 |
| 235 | +- indexOf方法 |
| 236 | +- remove方法 |
| 237 | + |
| 238 | +与实现ArrayList的名字一样,为SimpleLinkedList。[源码地址](https://github.com/byhieg/JavaTutorial/blob/master/src/main/java/cn/byhieg/collectiontutorial/listtutorial/SimpleLinkedList.java),欢迎star,fork |
| 239 | + |
| 240 | + |
| 241 | +## 构建一个双向链表 |
| 242 | + |
| 243 | +构建的代码如下: |
| 244 | + |
| 245 | +``` |
| 246 | +
|
| 247 | + private static class Node<E>{ |
| 248 | + E item; |
| 249 | + Node<E> next; |
| 250 | + Node<E> prev; |
| 251 | +
|
| 252 | + public Node(E item, Node<E> next, Node<E> prev) { |
| 253 | + this.item = item; |
| 254 | + this.next = next; |
| 255 | + this.prev = prev; |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | +``` |
| 260 | + |
| 261 | +常规的双向链表的构建方法,一个数字域存放数组,一个前指针指向一个Node类型的元素,一个后指针指向一个Node类型的元素。 |
| 262 | + |
| 263 | +对于LinkedList的实现而言,还需要以下三个成员变量 |
| 264 | + |
| 265 | +``` |
| 266 | +
|
| 267 | + private int size; |
| 268 | +
|
| 269 | + private Node<E> first; |
| 270 | +
|
| 271 | + private Node<E> last; |
| 272 | + |
| 273 | +``` |
| 274 | + |
| 275 | +## Add方法 |
| 276 | +这里实现的add方法是简单的add(E e)以及add(int index,E e)两个方法,addAll()将其他集合转换LinkedList的方法,暂时放到以后去实现。 |
| 277 | + |
| 278 | +add方法两个重载方法,其分别对应不同的添加方式。先说add(E e)方法的实现。 |
| 279 | + |
| 280 | +``` |
| 281 | +
|
| 282 | + public boolean add(E element) { |
| 283 | + addAtLast(element); |
| 284 | + return true; |
| 285 | + } |
| 286 | +
|
| 287 | +``` |
| 288 | +不指定位置添加元素,则默认添加到了链表的最后。addAtLast的核心代码如下: |
| 289 | + |
| 290 | +``` |
| 291 | +
|
| 292 | + private void addAtLast(E element) { |
| 293 | + Node<E> l = last; |
| 294 | + Node<E> node = new Node<E>(element, null, l); |
| 295 | + last = node; |
| 296 | + if (l == null) { |
| 297 | + first = node; |
| 298 | + } else { |
| 299 | + l.next = node; |
| 300 | + } |
| 301 | + size++; |
| 302 | + } |
| 303 | +
|
| 304 | +``` |
| 305 | + |
| 306 | +首先找到最后一位的Node元素,然后根据`element`创建一个新的Node元素,其next指向为null,prev指向为最后一位Node元素。在创建完Node元素之后,last就变成了先创建的Node元素,接下来只需要把新node元素加到链表中即可。即让l对象(原最后一位,现倒数第二位元素的next指针,指向新node元素)。至此,新node元素的next指向null,prev指向倒数第二个元素,倒数第二个元素的next指向新node,就将node成功加入链表。 |
| 307 | + |
| 308 | +上述的操作也可以看出,其插入的操作非常省时间,比起ArrayList,扩容,移动元素快很多。 |
| 309 | + |
| 310 | +add的第二个重载方法 add(int index ,E e),先看代码实现: |
| 311 | + |
| 312 | +``` |
| 313 | +
|
| 314 | + public void add(int index, E element) { |
| 315 | + checkRangeForAdd(index); |
| 316 | + if (index == size) { |
| 317 | + addAtLast(element); |
| 318 | + } else { |
| 319 | + Node<E> l = node(index); |
| 320 | + addBeforeNode(element, l); |
| 321 | + } |
| 322 | + } |
| 323 | + |
| 324 | +``` |
| 325 | + |
| 326 | +首先判断要插入的index是否在范围内,在的话,再执行后续的add操作。如果要插入的index刚好是最后一位,则执行上面讲的addAtLast,如果不是,则得到index所对应的Node元素,执行addBeforeNode。 |
| 327 | +获取index所对应的Node元素,是node方法,代码如下: |
| 328 | + |
| 329 | +``` |
| 330 | +
|
| 331 | + private Node<E> node(int index) { |
| 332 | + if (index < (size << 1)) { |
| 333 | + Node<E> cursor = first; |
| 334 | + for (int i = 0; i < index; i++) { |
| 335 | + cursor = cursor.next; |
| 336 | + } |
| 337 | + return cursor; |
| 338 | + } else { |
| 339 | + Node<E> cursor = last; |
| 340 | + for (int i = size - 1; i > index; i--) { |
| 341 | + cursor = cursor.prev; |
| 342 | + } |
| 343 | + return cursor; |
| 344 | + } |
| 345 | + } |
| 346 | +
|
| 347 | +``` |
| 348 | +这里的查找采用二分查找,节省查找时间,而且也应用到了双向链表的特点。首先判断index在前一半的范围内,还是后一半的范围内。如果是前一半,则游标Node初始为first,用游标Node元素的next,不断指向index所在的元素。如果是后一半,则游标Node初始为last,用游标Node元素的prev,不断指向index所在的元素。 |
| 349 | + |
| 350 | +在指定元素的前面插入新节点的addBeforeNode的方法如下: |
| 351 | + |
| 352 | +``` |
| 353 | +
|
| 354 | + private void addBeforeNode(E element, Node<E> specifiedNode) { |
| 355 | + Node<E> preNode = specifiedNode.prev; |
| 356 | + Node<E> newNode = new Node<>(element, specifiedNode, preNode); |
| 357 | + if (preNode == null) { |
| 358 | + first = newNode; |
| 359 | + } else { |
| 360 | + preNode.next = newNode; |
| 361 | + } |
| 362 | + specifiedNode.prev = newNode; |
| 363 | + size++; |
| 364 | + } |
| 365 | +
|
| 366 | +``` |
| 367 | + |
| 368 | +插入的方式很简单,新节点的prev是原index元素的prev,新节点的next是原index元素。剩下的操作是把该node放到链表中,让原index元素的prev的next为新节点,但是要判断preNode是不是空,是的话,表示newNode为第一个元素,就是first。 |
| 369 | + |
| 370 | +至此,一个add方法,就实现完了。 |
| 371 | + |
| 372 | +## get方法 |
| 373 | + |
| 374 | +get方法在有了上述node方法之后,就非常的简单。代码如下: |
| 375 | + |
| 376 | +``` |
| 377 | + public E get(int index) { |
| 378 | + checkRange(index); |
| 379 | + return node(index).item; |
| 380 | + } |
| 381 | +
|
| 382 | +``` |
| 383 | + |
| 384 | +checkRange检查index是否不在范围内。 |
| 385 | + |
| 386 | +``` |
| 387 | + private void checkRange(int index) { |
| 388 | + if (index >= size || index < 0) { |
| 389 | + throw new IndexOutOfBoundsException("指定index超过界限"); |
| 390 | + } |
| 391 | + } |
| 392 | +
|
| 393 | +``` |
| 394 | + |
| 395 | +## indexOf方法 |
| 396 | + |
| 397 | +indexOf(Object o)用来得到指定元素的下标。 |
| 398 | + |
| 399 | +``` |
| 400 | +
|
| 401 | + public int indexOf(Object element) { |
| 402 | + Node<E> cursor = first; |
| 403 | + int count = 0; |
| 404 | + while (cursor != null) { |
| 405 | + if (element != null) { |
| 406 | + if (element.equals(cursor.item)) { |
| 407 | + return count; |
| 408 | + } |
| 409 | + }else{ |
| 410 | + if (cursor.item == null) { |
| 411 | + return count; |
| 412 | + } |
| 413 | + } |
| 414 | + count ++; |
| 415 | + cursor = cursor.next; |
| 416 | + } |
| 417 | + return -1; |
| 418 | + } |
| 419 | + |
| 420 | +
|
| 421 | +``` |
| 422 | +与ArrayList一样,从第一位开始查找,首先先判断element是不是null,分成两种情况。 |
| 423 | + |
| 424 | +## remove方法 |
| 425 | +remove方法与add方法一样,同样有两个重载的方法,remove(Object o)与remove(int index) |
| 426 | + |
| 427 | +先看简单的remove(int index)方法,代码如下: |
| 428 | + |
| 429 | +``` |
| 430 | +
|
| 431 | + public E remove(int index) { |
| 432 | + checkRange(index); |
| 433 | + return deleteLink(index); |
| 434 | + } |
| 435 | +
|
| 436 | +``` |
| 437 | + |
| 438 | +deleteLink是将该index所对应的节点的链接删除的方法,其代码如下: |
| 439 | + |
| 440 | +``` |
| 441 | +
|
| 442 | + private E deleteLink(int index) { |
| 443 | + Node<E> l = node(index); |
| 444 | + E item = l.item; |
| 445 | + Node<E> prevNode = l.prev; |
| 446 | + Node<E> nextNode = l.next; |
| 447 | +
|
| 448 | + if (prevNode == null) { |
| 449 | + first = nextNode; |
| 450 | + }else{ |
| 451 | + prevNode.next = nextNode; |
| 452 | + l.next = null; |
| 453 | + } |
| 454 | +
|
| 455 | + if (nextNode == null) { |
| 456 | + last = prevNode; |
| 457 | + }else{ |
| 458 | + nextNode.prev = prevNode; |
| 459 | + l.prev = null; |
| 460 | + } |
| 461 | + size--; |
| 462 | + l.item = null; |
| 463 | + return item; |
| 464 | + } |
| 465 | +
|
| 466 | +``` |
| 467 | + |
| 468 | +首先获得该index对应的Node元素,得到该Node元素的前一个元素和后一个元素。接下来,只需要将前一个元素和后一个元素直接相连即可,其他只需要额外判断前一个元素和后一个元素是否为null就行。在判断前一个元素是否为null的时候,只需要操作前一个元素,在判断后一个元素是否为null的时候,也只需要操作后一个元素。最后,将要删除的元素各个引用至为null。 |
| 469 | + |
| 470 | +remove另一个重载方法remove(Object o),在实现了indexOf和deleteLink方法之后,就非常简单。 |
| 471 | + |
| 472 | +``` |
| 473 | +
|
| 474 | + public boolean remove(Object o) { |
| 475 | + int index = indexOf(o); |
| 476 | + if (index < 0){ |
| 477 | + return false; |
| 478 | + } |
| 479 | + deleteLink(index); |
| 480 | + return true; |
| 481 | + } |
| 482 | +
|
| 483 | +``` |
| 484 | + |
| 485 | +获取该元素对应对应的下标,然后执行deleteLink方法,完成remove操作。 |
| 486 | + |
| 487 | +## 总结 |
| 488 | +至此,一个功能简单的LinkedList就实现完成了,全部的代码可以看[源码地址](https://github.com/byhieg/JavaTutorial/blob/master/src/main/java/cn/byhieg/collectiontutorial/listtutorial/SimpleLinkedList.java),欢迎star,fork。 |
| 489 | + |
| 490 | + |
| 491 | + |
| 492 | + |
212 | 493 |
|
0 commit comments