什么是java链表_java链表初识

链表 Linked List

什么是链表?

真正的动态数据结构

最简单的动态数据结构

更深入的理解引用(或者指针)

更深入理解递归

辅助组成其他数据结构

!

b332cd307267

1572438319545.png

最后一个节点一定是Null

链表的优缺点:

优点:真正的动态,不需要处理固定容量的问题

缺点:丧失了随机访问的能力

对于数组来说,数组在内存中开辟的空间是连续的,可以直接寻找索引的偏移,找到元素的地址,该操作时间复杂度是O(1)。

对于链表来说,链表依赖next来连接各个节点,节点的位置在内存中是随机的,必须依赖next来寻找各个元素。

随机访问:

随机访问是说你可以随意访问该数据结构中的任意一个节点,假设该数据结构有10个节点,你可以随意访问第1个到第10个节点。

对于链表而言,如果其存在10个节点,如果你要访问第5个节点,你只能从列表的头或者尾,依次遍历相邻的每一个节点;对于vector而言,你可以直接利用[]操作符,直接访问[4],不需要遍历其他的节点,这就是随机访问。

**比如first是第一个元素的地址,现在想访问第N个元素。 随机访问:直接first+N,便可以得到第N个元素的地址,因为这些相邻元素是按顺序连续存储的。 **

比如普通数组就是可随机访问的。而链表不支持随机访问,链表存储的元素,它们的存储地址也不是连续的,是随机的。

要想访问第N个元素,只能从second = first->next遍历第2个元素,然后再three = first->next遍历第3个元素… 这样一直到第N个元素。所以这样的访问速度就没有随机访问快。

b332cd307267

1572439095562.png

链表类中嵌套节点类

public class LinkedList {

/**

* 组成链表内部的节点类

* 使用private,屏蔽底层实现细节,用户无需知道

*/

private class Node{

public E e;

public Node next;

//节点构造函数传入储存的元素,与指向的下一节点的内存地址

public Node(E e,Node next){

this.e=e;

this.next=next;

}

public Node(E e){ this(e,null); }

public Node(){this(null,null);}

@Override

public String toString(){

return e.toString();

}

}

public Node head;//链表头结点

private int size;//链表实际容量

public LinkedList(){

head = null;

size = 0;

}

//获取链表中的元素个数

public int getSize(){return size;}

//判断是否为空

public boolean isEmpty(){ return size == 0;}

}

在链表中添加元素

在链表头添加元素

创建一个新的节点node,并且存储要添加的元素

将node指向head

将node变成head

//在链表头添加元素

public void addFirst(E e){

/* Node node = new Node(e);

node.next = head;

head = node;*/

//以上三行代码的优化写法

//1、新创建的Node中元素为e,指向节点是head

//2、将新创建的Node赋值给head,head指向头结点

head = new Node(e,head);

size ++;

}

b332cd307267

在链表头添加节点.gif

根据索引在链表中添加元素

仅供练习使用,链表一般不会有索引

创建要插入的元素node

创建插入位置的前一个节点prev

通过index找到prev

将node指向的下一个元素改为prev指向的下一个元素

将prev指向的下一个元素指向node

注意:==第4 第5步顺序不能换==

//在链表的(0-based)位置添加新的元素e

//在链表中不是一个常规的操作,仅练习用

public void add(E e,int index){

if(index < 0 || index > size)

throw new IllegalArgumentException("Add failed. Illegal index.");

if(index == 0)

addFirst(e);

else{

Node prev = head;

for (int i = 0; i < index-1; i++) {

prev = prev.next;

}

/*Node node = new Node(e);

node.next = prev.next;

prev.next = node;*/

//创建一个新节点,节点中元素为e,指向的下一个元素是prev.next(prev指向的下一个节点)

//将prev.next指向新创建的节点,该节点成功插入prev和prev.next之间

prev.next = new Node(e,prev.next);

size++;

}

}

b332cd307267

根据索引在链表中添加元素.gif

在链表末尾添加元素(直接使用add(E e,size))

为链表设立虚拟头节点

在添加元素中遇到的问题:

​ 在链表任意位置添加元素,在链表索引位置添加与链表头处添加,逻辑不同

产生原因:链表头节点之前无节点

解决办法:为表设立虚拟头节点(dummyHead)

虚拟头结点为空,无意义,只是为了编写逻辑方便

==链表的第一个节点是dummyHead.next==

类似循环队列中有意浪费一个节点

b332cd307267

![1573011628788.png](https://upload-images.jianshu.io/upload_images/19955166-2df7d3f297c67338.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

public Node dummyHead;

private int size;

public LinkedList(){

dummyHead = new Node(null,null);

size = 0;

}

//在链表的(0-based)位置添加新的元素e

//在链表中不是一个常规的操作,仅练习用

public void add(E e,int index){

if(index < 0 || index > size)

throw new IllegalArgumentException("Add failed. Illegal index.");

Node prev = dummyHead;

//利用dummyHead是在链表第一个有效节点前添加了一个元素

//所以for的界限要+1,prev需多走一步

for (int i = 0; i < index; i++) {

prev = prev.next;

}

/*Node node = new Node(e);

node.next = prev.next;

prev.next = node;*/

//创建一个新节点,节点中元素为e,指向的下一个元素是prev.next(prev指向的下一个节点)

//将prev.next指向新创建的节点,该节点成功插入prev和prev.next之间

prev.next = new Node(e,prev.next);

size++;

}

//在链表头添加元素

public void addFirst(E e){

add(e,0);

}

//在链表末尾添加元素

public void addLast(E e){

add(e,size);

}

链表的遍历,查询和修改

查找的是当前元素,所以命名为cur

//在链表的(0-based)位置获取新的元素e

//在链表中不是一个常规的操作,仅练习用

public E get(int index){

//Node prev = dummyHead;

//直接获取第index位的元素,不是index位置的前一个元素

Node cur = dummyHead.next;

for (int i = 0; i < index; i++)

cur = cur.next;

return cur.e;

}

//获取链表第一个元素

public E getFirst(){

return get(0);

}

//获取链表最后一个元素

public E getLast(){

return get(size-1);

}

//修改链表的(0-based)位置的元素e

//在链表中不是一个常规的操作,仅练习用

public void set(int index, E e){

Node cur = dummyHead.next;

for (int i = 0; i < index; i++)

cur = cur.next;

cur.e=e;

}

//查找链表中是否有元素e

public boolean constains(E e){

Node cur = dummyHead.next;

while(cur != null){

if(cur.e.equals(e))

return true;

cur = cur.next;

}

return false;

}

链表元素的删除

b332cd307267

删除索引元素.gif

常见错误

b332cd307267

1573011628788.png

实际只是将cur指向了下一个元素,需要发生变化的是要删除节点的前一个节点

//删除链表的(0-based)位置的元素e

//在链表中不是一个常规的操作,仅练习用

public E remove(int index){

Node prev = dummyHead;

for (int i = 0; i < index ; i++) {

prev = prev.next;

}

Node delNode = prev.next;

prev.next = delNode.next;

delNode.next = null;

size --;

return delNode.e;

}

public E removeFirst(){

return remove(0);

}

public E removeLast(){

return remove(size-1);

}

链表的时间复杂度分析

添加操作 O(n)

方法名

时间复杂度

addLast(e)

O(n)

addFirst(e)

O(1)

add(index,e)

O(n/2)=O(n)

删除操作 O(n)

方法名

时间复杂度

removeLast(e)

O(n)

removeFirst(e)

O(1)

remove(index,e)

O(n/2)=O(n)

修改操作 O(n)

set(index,e) O(n)

查找操作 O(n)

get(index) O(n)

contains(e) O(n)

总结

b332cd307267

1573021820979.png

使用链表实现栈

public class LinkedListStack implements Stack {

private LinkedList list;

public LinkedListStack(){list = new LinkedList<>();}

@Override

public int getSize() { return list.getSize();}

@Override

public boolean isEmpty() {return list.isEmpty(); }

//入栈

@Override

public void push(E e) {list.addFirst(e);}

//出栈

@Override

public E pop() {return list.removeFirst();}

@Override

public E peek() { return list.getFirst();}

@Override

public String toString(){

StringBuilder res = new StringBuilder();

res.append("Stack: top ");

res.append(list);

return res.toString();

}

}

使用数组实现栈和链表实现栈的比较

public class MainTest {

//测试使用stack运行opCount个push和pop操作所需要的时间,单位:秒

private static double testStack(Stack stack, int opCount) {

long startTime = System.nanoTime();//获取当前时间

Random random = new Random();

for (int i = 0; i < opCount; i++)

stack.push(random.nextInt(Integer.MAX_VALUE));

for (int j = 0; j < opCount; j++)

stack.pop();

long endTime = System.nanoTime();//获取完成操作后的当前时间

return (endTime - startTime) / 1000000000.0;

}

public static void main(String[] args){

int opCount = 1000000;

//使用数组完成栈的操作,消耗时间点:扩容

ArrayStack arrayStack = new ArrayStack<>();

double time1 = testStack(arrayStack,opCount);

System.out.println("ArrayStack, time: " + time1+"s");

//使用链表完成栈的操作,消耗时间点:new操作

LinkedListStack LinkedListStack = new LinkedListStack<>();

double time2 = testStack(LinkedListStack,opCount);

System.out.println("LinkedListStack, time: " + time2+"s");

/*结果:

opCpount:100000

ArrayStack, time: 0.0779552s

LinkedListStack, time: 0.045245s

opCpount:1000000

ArrayStack, time: 0.2513879s

LinkedListStack, time: 0.8988562s*/

}

}

使用链表实现队列

b332cd307267

1573031846652.png

由于没有dummyHead,要注意链表为空的情况

public class LinkedLIstQueue implements Queue {

private class Node{

public E e;

public Node next;

public Node(E e,Node next){

this.e = e;

this.next = next;

}

public Node(E e){ this(e,null);}

public Node(){ this(null,null);}

public String toString(){ return e.toString();}

}

private Node head,tail;

private int size;

public LinkedLIstQueue(){

head = null;

tail = null;

size = 0;

}

@Override

public int getSize() {

return size;

}

@Override

public boolean isEmpty() {

return size == 0;

}

@Override

public void enqueue(E e) {

if(tail==null){

tail=new Node(e);

head = tail;

}else{

tail.next=new Node(e);

tail=tail.next;

}

size ++;

}

@Override

public E dequeue() {

if(isEmpty())

throw new IllegalArgumentException("链表为空");

Node retNode = head;

head = head.next;

retNode.next = null;

if(head == null)

tail = null;

size --;

return retNode.e;

}

@Override

public E getFornt() {

if(isEmpty())

throw new IllegalArgumentException("链表为空");

return head.e;

}

@Override

public String toString(){

StringBuilder sb = new StringBuilder();

sb.append("Queue: front ");

for (Node cur = head; cur != null; cur=cur.next)

sb.append(cur + "->");

sb.append("NULL tail");

return sb.toString();

}

}

对100000个元素进行操作,数组队列,循环队列和链表完成的队列的对比

b332cd307267

1573046387080.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值