java linkedlist 查找_Java LinkedList小记

1. 基本用法

LinkedList实现了List、Deque、Queue接口,可以按照队列、栈和双端队列的方式进行操作。LinkedList有两个构造方法,一个是默认构造,另一个接受Collection:

publicLinkedList()public LinkedList(Collection extends E> c)

可以按照List操作:

List list = new LinkedList<>();

List list1 = new LinkedList<>(Arrays.asList(2, 3, 4, 5));

LinkedList还实现了队列接口Queue,队列的特点是先进先出,在尾部添加数据,在头部删除数据,其接口定义为:

public interface Queue extends Collection{//在尾部添加元素

booleanadd(E e);//在尾部添加元素

booleanoffer(E e);//返回头部元素,并且从队列中删除

E remove();//返回头部元素,并且从队列中删除

E poll();//返回头部元素,但不改变队列

E element();//返回头部元素,但不改变队列

E peek();

}

Queue接口扩展了Collection,主要有三种操作,在尾部添加数据(add、offer)、查看头部元素(element、peek)和删除头部元素(remove、poll)。每种操作都有两种形式,区别在于特殊情况的处理不同。特殊情况是指当队列为空或者为满时,为空就是没有元素数据,为满是指队列有长度大小限制,而且已经占满了。LinkedList的实现中,队列长度没有限制,但是其他的Queue的实现可能有。在队列为空时,remove和element会抛出异常NoSuchElementException,而poll和peek返回null;在队列为满时,add会抛出IllegalStateException,而offer只是返回false。

把LinkedList当做Queue使用:

Queue queue = new LinkedList<>();

queue.offer("a");

queue.offer("b");

queue.offer("c");while (queue.peek() != null) {

System.out.println(queue.poll());

}

栈是一种和队列特点相反的数据结构,它的特点是先进后出,后进先出。Java中没有单独的栈接口,栈的相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:

//入栈

voidpush(E e);//出栈

E pop();//查看

E peek();

push表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push会抛出IllegalStateException;pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空会抛出NoSuchElementException;peek查看栈头部元素,不修改栈,如果栈为空,返回特殊值null。使用方法如下:

Deque stack = new LinkedList<>();

stack.push(1);

stack.push(2);

stack.push(3);while (stack.peek() != null) {

System.out.println(stack.pop());

}/*** output:

* 3

* 2

* 1*/

Java中还有一个Stack类,就是栈的意思,它也实现了栈的一些方法,如push、pop、peek等,但它没有实现Deque接口,他是Vector的子类它增加的这些方法也通过synchronized实现了线程安全。由于Vector和Stack内部使用了大量的syncronized做同步操作,效率比较低,已经过时了,具体就不学习了。

栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但只在尾部添加、头部只查看和删除元素。有一个更为通用的操作两端的接口Deque。接口定义如下:

public interface Deque extends Queue{voidaddFirst(E e);voidaddLast(E e);booleanofferFirst(E e);booleanofferLast(E e);

E removeFirst();

E removeLast();

E pollFirst();

E pollLast();

E getFirst();

E getLast();

E peekFirst();

E peekLast();//删除第一次出现的指定元素(从头到尾遍历)

booleanremoveFirstOccurrence(Object o);//删除最后次出现的指定元素(从头到尾遍历)

booleanremoveLastOccurrence(Object o);booleanadd(E e);booleanoffer(E e);

E remove();

E poll();

E element();

E peek();voidpush(E e);

E pop();booleanremove(Object o);//队列是否包含指定元素

booleancontains(Object o);public intsize();

Iteratoriterator();//从后往前遍历的迭代器

IteratordescendingIterator();

}

根据方法名很容易知道作用,稍微不太清晰的做了注释,descendingIterator()方法示例如下:

Deque deque = new LinkedList<>(Arrays.asList("a", "b", "c", "d"));

Iterator it =deque.descendingIterator();while(it.hasNext()) {

System.out.print(it.next()+ " ");

}/*** output:

* d c b a*/

下面看下实现原理。

2. 原理

先来看下LinkedList的内部组成,再分析一些主要方法的实现,代码基于JDK8。

2.1内部组成

我们知道,ArrayList内部是数组,元素在内存中是连续存放的,基于索引的访问效率非常高,但LinkedList不是。LinkedList的内部实现是双向链表,每个元素在内存中都是单独存放的,元素之间通过链接连接在一起。为了表示链接关系,需要一个节点的概念。节点包括实际的元素,但同时有两个链接,分别指向前一个(前驱)和后一个节点(后继)。节点是一个内部类:

private static class Node{

E item;

Nodenext;

Nodeprev;

Node(Node prev, E element, Nodenext) {this.item =element;this.next =next;this.prev =prev;

}

}

Node类表示节点,item指向实际的元素,next后一个节点,prev指向前一个节点。LinkedList内部组成就是如下三个实例变量:

transient int size = 0;transient Nodefirst;transient Node last;

size表示链表长度,默认为0,first指向头节点,last指向尾节点,初始值都是null。LinkedList的所有public方法内部操作的就是这三个实例变量,来看下具体方法:

2.2 add方法

public booleanadd(E e) {

linkLast(e);return true;

}voidlinkLast(E e) {//将尾节点赋给l变量

final Node l =last;//新建节点,将l赋给新建节点的pre前驱节点,e为当前节点的元素值,新建节点没有后继节点,所以为null

final Node newNode = new Node<>(l, e, null);//将新建节点赋给尾节点

last =newNode;//如果尾节点不存在,就将新建节点作为头结点赋给first实例变量

if (l == null)

first=newNode;//如若尾节点存在,就将新建节点作为尾节点的后继节点赋给l.next

elsel.next=newNode;//链表长度加1

size++;//修改次数加1

modCount++;

}

代码的基本步骤见代码中注释,modCount变量用来记录修改次数,便于在迭代中检测结构性变化。我们根据图示来理解下。比如如下代码:

List list = new LinkedList();

list.add("a");

list.add("b");

a4cba27bc442f1cc4ef49a3b09a5b445.png

当新建list对象后内部结构如图一,头结点和尾节点都是null;当添加“a”后内部结构如图二,size加1,头结点和尾节点都指向同一个Node节点;当添加完“b”后内部结构如图三所示。

2.3 根据索引访问元素

来看下get方法:

public E get(intindex) {//检查索引位置的有效性,若无效,抛出异常

checkElementIndex(index);//索引有效,执行node方法,查找指定索引位置的元素并返回

returnnode(index).item;

}private void checkElementIndex(intindex) {if (!isElementIndex(index))//抛出未受检异常,索引越界异常

throw newIndexOutOfBoundsException(outOfBoundsMsg(index));

}private boolean isElementIndex(intindex) {return index >= 0 && index

}

Node node(intindex) {//若索引位置在前半部分,则从头结点开始查找(右移一位相当于除以2),若找到返回节点

if (index < (size >> 1)) {

Node x =first;for (int i = 0; i < index; i++)

x=x.next;returnx;//从尾节点向前找

} else{

Node x =last;for (int i = size - 1; i > index; i--)

x=x.prev;returnx;

}

}

与ArrayList不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头到尾顺着连接查找,效率比较低。

2.4 按内容查找元素

看下indexOf的代码:

public intindexOf(Object o) {int index = 0;//查找元素为null时

if (o == null) {for (Node x = first; x != null; x =x.next) {if (x.item == null)returnindex;

index++;

}//查找元素不为null时

} else{for (Node x = first; x != null; x =x.next) {if(o.equals(x.item))returnindex;

index++;

}

}//买找到指定元素返回-1

return -1;

}

代码比较简单,有两种情况,都是从头节点开始找,见代码注释。

2.5 插入元素

add是在尾部添加元素,如果在头部或者中间插入元素可以使用如下重载方法:

public void add(intindex, E element) {

checkPositionIndex(index);//这就是在尾部添加元素

if (index ==size)

linkLast(element);//主要看这个

elselinkBefore(element, node(index));

}void linkBefore(E e, Nodesucc) {//succ不为空,就把succ的前驱节点赋给pred

final Node pred =succ.prev;//新建节点,将pred指定为新建节点的前驱节点,succ为后继节点

final Node newNode = new Node<>(pred, e, succ);//将后的前驱指向新建节点

succ.prev =newNode;//将前驱的后继指向新建节点,若前驱为空,修改头结点指向新节点

if (pred == null)

first=newNode;elsepred.next=newNode;//增加长度

size++;

modCount++;

}

下面通过图示来加深理解,比如添加一个元素

list.add(1, "c");

43a0e7728522104037acbe9b351d5c53.png

可以看出,在中间插入元素,LinkedList只需按需分配内存,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多的额外空间,且移动、复制所有后继元素。

2.6 删除元素

再来看看删除元素的代码:

public E remove(intindex) {//同上检查索引是否有效

checkElementIndex(index);//node方法先查找节点,再执行unlink删除指定节点

returnunlink(node(index));

}

E unlink(Nodex) {//x节点不为空,取得节点元素值

final E element =x.item;//取得前驱节点

final Node next =x.next;//取得后继节点

final Node prev =x.prev;//指定前驱的后继为x的后继(不在指向x),若前驱为空,修改头节点指向x的后继

if (prev == null) {

first=next;

}else{

prev.next=next;

x.prev= null;

}//指定后继的前驱为x的前驱(不再指向x),若后继为空,修改尾节点指向x的前驱

if (next == null) {

last=prev;

}else{

next.prev=prev;

x.next= null;

}//x节点元素值设为null,便于垃圾回收

x.item = null;//链表长度减1

size--;//修改次数加1

modCount++;//返回删除的节点值

returnelement;

}

分析逻辑见代码注释,基本思路就是让x的前驱和后继直接链接起来,再把x的前驱、后继节点、item都设置为null,便于垃圾回收。下面通过图示加深理解,比如删除一个元素:

list.remove(1);

0a1f01019e23323d8a1e192121dc1721.png

3. LinkedList特点总结

用法上LinkedList是一个List,有序有重复元素,也实现了Deque接口,可以作为队列、栈和双端队列使用。实现原理上,LinkedList内部是一个双向链表,并维护了长度、头结点和尾节点。有如下特点:

1. 按需分配空间,不需要预先分配很多空间。

2. 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。

3. 不管列表是否已排序,只要按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。

---------- I love three things in this world. Sun, moon and you. Sun for morning, moon for night , and you forever .

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值