java集合—— 链表(java中的所有链表都是双向链表)

【0】README

0.1) 本文描述转自 core java volume 1, 源代码 diy 的, 旨在理解 java集合—— 链表(java中的所有链表都是双向链表) 的相关知识;
0.2) for full source code , please visit https://github.com/pacosonTang/core-java-volume/blob/master/chapter13/LinkedListTest.java


【1】链表(java中的所有链表都是双向链表)

1.1)数组和数组列表有一个重大缺陷: 这就是从数组的中间位置删除一个元素要付出很大的代价, 其原因是数组中处于被删除元素之后的所有元素都要向数组的前段移动。(在数组中插入一个元素也是如此)
1.2)解决方法: java引入了链表来解决这个问题。在java的程序设计语言中, 所有链表实际上都是双向链接的——即每个结点还存放着指向前驱节点的引用;
1.3)看个荔枝:(先添加3个元素,然后再将第2个元素删除)

List<String> staff = new LinkedList<>();
staff.add("amy");
staff.add("bob");
staff.add("carl");
Iterator i1 = stafff.iterator();
String first = i1.next(); // visit first element
String second = i1.next(); // visit second element
i1.remove(); // remove last visited element

这里写图片描述
1.4)链表与泛型集合之间有一个重要区别

  • 1.4.1)链表有一个有序集合,每个对象的位置都十分重要: LinkedList.add 方法将对象添加到链表的尾部, 但是,常常需要将元素添加到链表中间。 由于迭代器是描述集合中位置的,所以这种依赖于位置的 add 方法将由迭代器负责, 只有对自然有序的集合使用迭代器添加元素才有实际意义;
  • 1.4.2)又如 集(Set)类型, 其中的元素完全是无序的。因此, 在 Iterator接口中就没有add 方法。相反,集合类库提供了子接口 ListIterator, 其中包含 add 方法(注意, Set是集类型 而 Collection是 集合类型,它们是不一样的,再次提醒):
    (现在你知道为什么要添加ListIterator接口 ? 因为Set是无序的,在Iterator中没有 add方法,而LinkedList是有序的,又需要add方法,所以就有添加了一个接口 ListIterator用于给 LinkedList列表添加add 方法)
interface ListIterator<E> extends Iterator<E>
{
    void add(E element);
    ...
}

这里写图片描述
对以上代码的分析(Analysis):

  • A1)与 Collection.add 不同, 这个方法不返回boolean类型的值, 它假定添加操作总会改变链表;
  • A2)另外,ListIterator接口有两个方法, 可以用来反向遍历链表(Methods):

    • M1)E previous(); : 与next方法一样, previous 方法返回越过的对象;
    • M2)boolean hasPrevious();
  • Attention)

    • A1)顺向遍历: next();
    • A2)逆向遍历: previous();

这里写图片描述
1.5)LinkedList 类的 listIterator方法返回一个实现了 ListIterator接口的迭代器对象:

ListIterator<String> iter = staff.listIterator();
  • 1.5.1)Add 方法在迭代器位置之前添加了一个新对象, 看个荔枝(下面的代码将越过链表中的第一个元素, 并在第二个元素之前添加 “tang”):
List<String> staff = new LinkedList<>();
staff.add("amy");
staff.add("bob");
staff.add("carl");
ListIterator<String> iter = staff.listIterator();
iter.next();
iter.add("tang");
// 越过第一个元素, 在第二个元素前添加 “tang”
  • 1.5.2)如果多次调用add方法, 将按照提供的次序把元素添加到链表中。 它们被依次添加到 迭代器当前位置之前;
  • 1.5.3)当用一个刚刚由 Iterator方法返回, 并且指向链表表头的迭代器调用add 方法时, 新添加的元素将变成列表的新表头;当迭代器越过链表的最后一个元素时(即hasNext 返回false), 添加的元素将变成列表的新表尾; (干货)
  • 1.5.4)如果链表有n个元素: 那么就会有n+1个位置可以添加新元素;
  • 1.5.5)看个荔枝:如果链表包含3个元素, A、B、C,就有4个位置(标有 |)可以插入新元素:
|ABC
A|BC
AB|C
ABC|

Annotation) (我只觉得有好多干货哦)

  • A1)在用光标类比时要格外小心。remove操作与 BACKSPACE 键的工作方式不太一样;在调用next方法之后, remove方法确实与 BACKSPACE 键一样删除了迭代器左侧的元素;
  • A2)但是,如果调用 previous就会将 右侧的元素删除掉,并且不能在同一行中调用两次remove;
  • A3) add 方法只依赖于迭代器的位置, 而 remove方法依赖于迭代器的状态;
  • A4)最后需要说明的是, set方法用一个新元素取代调用 next 或 previous 方法返回的上一个元素;
    • A4.1)看个荔枝:(用一个新值取代链表中的第一个元素)
ListIterator<String> iter = list.listIterator();
String oldValue = iter.next(); // returns first element
iter.set(newValue); // sets first element to newValue

这里写图片描述
1.6)出现的问题:

  • 1.6.1)可以想象,如果在某个迭代器修改集合时,另一个迭代器对其进行遍历, 一定会出现混乱的状况;
  • 1.6.2)链表迭代器使其能够检测到这种修改。如果迭代器发现他的集合被另一个迭代器修改了, 或是被该集合自身的方法修改了, 就会抛出一个 Concurrent ModificationException 异常,
  • 1.6.3)看个荔枝:
List<String> list = ...;
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator(); //两个迭代器指向同一个集合;
iter1.next();
iter1.remove();
iter2.next(); // throws ConcurrentModificationException; 因为iter1的第一个元素已经被remove 了;

这里写图片描述
1.7)解决方法:

  • 1.7.1)为了避免发生并发修改异常, 请遵循以下规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读也能写的迭代器;
  • 1.7.2)有一种简单的方法可以检测并发修改的问题:集合可以跟踪改写操作(添加或删除元素)的次数。每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检查自己修改操作的计数值是否与集合的改写操作计数值一致。如果不一致, 抛出一个 ConcurrentModificationException异常;

Annotation) 对于并发修改列表的检测有一个奇怪的例外。链表只负责跟踪对列表的结构性修改, 例如, 添加元素,删除元素。set操作不被视为结构性修改;
Conclusion)

  • C1)可以使用 ListIterator类从前后两个方向遍历链表中的元素, 并可以添加和删除元素;
  • C2) Collection接口中声明了许多用于对链表操作的有用方法。其中大部分方法都是在LinkedList 类的超类 AbstractCollection中实现的;
  • C3)在java类库中, 还存在着一些争议的方法。

    • C3.1)链表不支持快速地随机访问。如果要查看链表中第n个元素, 就必须从头开始, 越过 n-1个元素。 没有捷径可走。鉴于这个原因, 在程序需要采用整数索引访问元素时, 程序员通常不选用链表;
    • C3.2)尽管如此,LinkedList 还是提供了用来访问特定元素的get方法:
LinkedList<String> list = ...;
String obj = list.get(n);
  • (以上代码效率低的一逼,不推荐使用;)

  • C4)迭代器还有一个方法, 可以告知当前位置的索引:

  • C4.1)由于java 迭代器指向两个元素间的位置, 所以可以同时产生两个索引: nextIndex方法返回下一次调用next方法时 返回元素的整数索引; previousIndex 方法返回下一次调用 previous 方法时返回元素的整数索引;

这里写图片描述

  • C4.2)以上两个方法执行效率非常高,因为迭代器保持着当前位置的计数值;(执行效率高,那就是干货)
  • C4.3)需要说明的是: 如果有一个整数索引n, list.listIterator(n) 将返回一个迭代器, 这个迭代器指向索引为 n 的元素前面的位置。也就是说, 调用 next 与 调用 list.get(n) 会产生同一个元素, 只是获得这个迭代器的效率比较低而已;
  • C5)我们建议避免使用以整数索引表示链表中位置的所有方法。 如果需要对集合进行随机访问, 就使用数组 或 ArrayList, 而不要使用链表;(干货建议)
  • C6)要知道, 使用链表的唯一理由是尽可能减少在列表中间插入和删除元素所付出的代价, 如果列表中只有少数几个元素,就完全可以用 ArrayList(循环数组); (干货建议)

这里写图片描述


API java.util.List<E> 1.2
ListIterator<E> listIterator():返回一个列表迭代器, 以便用来访问列表中的元素;
ListIterator<E> listIterator(int index):返回一个列表迭代器, 以便用来访问列表中的元素; 这个元素是第一次调用next 返回的给定索引的元素;
void add(int i, E element) 
void addAll(int i, Collection<? extends E> elements)
E remove(int i)
E get(i)
E set(int i, E element)
int indexOf(Object element) : 返回与指定元素相等 的元素在列表中第一次出现的位置, 如果没有这样的元素将返回-1;
int lastIndexOf(Object element):返回与指定元素相等 的元素在列表中最后次出现的位置, 如果没有这样的元素将返回-1;

API java.util.ListIterator<E> 1.2
void add(E element)
void set(E element)
boolean hasPrevious(): 当反向迭代列表时, 还有可供访问的元素;
E previous() :返回前一个对象。如果已经达到了 列表的头部, 就抛出一个 NoSuchElementException
int nextIndex():返回下一次调用 next 方法时将返回的元素索引;
int previousIndex(): 返回下一次调用 previous方法时将返回的元素索引;

API java.util.LinkedList<E> 1.2
LinkedList()
构造一个空链表;

LinkedList(Collection<? extend E> elements)
构造一个链表, 并将集合中的所有元素添加到这个链表中;

void addFirst(E element)
void addLast(E ele)
将某个元素添加到头部或尾部;

E getFirst()
E getLast()
返回头部或尾部的元素;

E removeFirst();
E removeLast();
删除并返回头部或尾部的元素;
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值