#Java源码分析--LinkedList容器(1)

2.7.1 链表的概念
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作。

2.7.2 链表的分类
从链表的实现方式可以把链表分为单链表,循环链表,双向链表。
单链表指的是链表中的元素的指向只能指向链表中的下一个元素或者为空,元素之间不能相互指向。也就是一种线性链表。
双向链表即是这样一个有序的结点序列,每个链表元素既有指向下一个元素的指针,又有指向前一个元素的指针,其中每个结点都有两种指针,即left和right。left指针指向左边结点,right指针指向右边结点。
循环链表指的是在单向链表和双向链表的基础上,将两种链表的最后一个结点指向第一个结点从而实现循环。

2.7.3 对链表的理解
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必按顺序存储,链表在插入的时候可以达到O⑴的复杂度,比另一种线性表:顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(n)和O⑴。使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。我们前面已经了解到,顺序表在做查询(get)操作时,是直接对数组的索引进行访问:
/***

  • @functionName:get
  • @description:通过索引获得顺序表的元素
  • @param index
  • @author yzh
  • @date 2018-12-31
  • @return
    */
    @Override
    @SuppressWarnings(“unchecked”)
    public E get(int index){
    //对太大或太小的索引使用异常抛出进行控制,对于顺序表而言,底层数组的有效索引区间为0-size
    if((myElementData == MYDEFAULT_ELEMENTDATA ) || ((size > 0) && index >= size) || index < 0){
    throw new RuntimeException(“illegal element index of myElementData”);
    }
    return (E) myElementData[index];
    }
    因此,顺序表的查询效率是非常高的,时间复杂度为O⑴。


顺序表的查询和插入操作的时间复杂度分别为(O(1)和O(n))。
从上面的分析我们知道,顺序表查询的时间复杂度为O(1),而在顺序表中,插入和删除元素操作,移动的情况是:0-n,因此一个元素平均需要移动:
(0+n)n/2n=n/2, 在一个长度为n的顺序表的表尾插入一个新元素的渐进时间复杂度为O (n)。这是因为不论是删除还是插入操作,都使用到了System.arraycopy(myElementData, index+1, myElementData, index, moveCapacityNum);当插入元素的位置在首位时,移动的元素的个数时n,当插入元素的位置在末端时,移动的元素个数为0,因此移动元素的个数区间为[0,n]。
2.7.4 单链表
指针是链表中的一个十分重要的概念,因此在理解链表之前,先来理解什么是指针、引用和引用指向对象。在c语言中初次接触到了指针的概念,我们知道,计算机内存中有两个核心概念:地址和内存单元的数据,它们都位于计算机内存中,内存地址相当于内存单元的编号,用一连串字符表示,正是通过这些内存地址来获取到内存单元中的数据,例如,int a = 1,int b = &a,(&表示取地址)可以通过b来获取内存单元中的数据1的地址,这里的变量*b就是存储了1对应的内存地址,所以可以通过这个地址获取到1这个值。内存单元是计算机存储数据的最小单位,以字节计数,一个内存单元的大小是一字节,也就是8比特,是一串8比特的二进制数。指针就是指向内存单元中(复合)数据的内存地址,指针本身是一个变量,它存储的是数据在内存栈中的地址,而不是存放在堆区中的值。这里强调一下,指针我们可以理解为一种特殊的数据类型,也就是复合数据类型。我们都知道,基本数据类型都放在栈区,这是因为基本数据类型数据占内存较少,而且栈区的数据可以“共享”,例如,int a = 5,int b = 5,计算机首先会在栈区的内存中开辟一个内存单元存储数字5(二进制形式存在),然后通过a对应的内存地址指向这个内存单元的数据5,当再次定义b=5后,计算机首先会在内存中找原来是否有数字5这个内存单元,如果有,就让当前开辟的地址直接指向这个内存单元的数字,这样a,b对应的两个地址就同时指向了这个内存单元的数据5,如下图所示:
在这里插入图片描述
另外,还有一个十分重要的因素,那就是在栈区访问数据的速度仅次于寄存器,快于访问堆区存储的数据,但是在栈区不能存储重量级数据类型,灵活性也不高。在堆区存储的数据的特点是重量级的复合类型或者说动态产生的数据,另外,堆区数据的特点是不能直接“共享“,例如:A a1 = new A();A a2 = new A();的内存分配如下所示:
在这里插入图片描述
我们看到,在堆区的同一个值,会在栈区开辟两个地址(引用),分别指向在堆区开辟的两个内存空间的数据(值相等),而不是两个地址指向堆区的同一个值,所以a1和a2这两个变量的值并不相等(存储的值是地址),在Java中的比较的就是在栈区的值,也就是变量本身的值,很多人会想不明白,不是说比较的是引用吗?而引用不是地址吗,那么int a = 1中的a和int b = 1的b为什么相等,它们不是各自在栈区中开辟了一个地址吗?确实是a、b各自在栈区开辟了一个地址,但是我在上面提到Java中的比较的就是在栈区的值,也就是变量本身的值,也就是说int a = 1中的a和int b = 1的b存储的是在栈区的值,而不是在栈区的地址,而对于对象而言,在栈区只分配了地址,真正的值是存储在堆区的,也就是说此时a1和a2存储的值是栈区的地址,由于开辟的地址是唯一的,因此a1!=a2。我们把上面的a1和a2存储的地址称之为指针,因此复合类型才能有指针的概念!指针指向的是堆区中真的数据。在Java中,指针就是引用,也就是a1,b1存储的栈区的地址就是引用,而存储在堆区的数据称之为A类的一个对象或实例,一个类是该同类对象的集合。所以,对象和引用并不是同一个概念!从上面的图可以看出,栈区的引用指向了堆区的对象,我们可以形象地描述为a1或b1引用指向A对应的对象。例如:
A a = new A();
A b = new A();
A c = a;
System.out.println( a == b);//false
System.out.println( c == a );//true
这个过程在内存中表示如下:
在这里插入图片描述
我们看到,a和c都拥有同一个地址,这意味着a和c是同一个引用,也就是a与c的引用值相等,因此使用
比较的结果相等,equals比较的是在堆区的对象值,所以结果也相等,因此,a和c的地址指向堆区的同一个对象,简称a和c指向同一个对象。一个完整的Java程序运行过程会涉及以下内存区域如下所示。
(1)寄存器:
JVM内部虚拟寄存器,存取速度非常快,程序不可控制。
(2)栈:
保存局部变量的值,包括:1.用来保存基本数据类型的值;2.保存类的实例,即堆区对象的引用(指针)。也可以用来保存加载方法时的帧。
(3)堆:
用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。
(4)常量池:
JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于堆中。
(5)代码段:
用来存放从硬盘上读取的源程序代码。
(6)数据段:
用来存放static定义的静态成员。
对于局部变量,如果是基本类型,会把值直接存储在栈;如果是引用类型,比如String s = new String(“william”);会把其对象存储在堆,而把这个对象的引用(指针)存储在栈。
再如
String s1 = new String(“william”);
String s2 = s1;
s1和s2同为这个字符串对象的实例,但是对象只有一个,存储在堆,而这两个引用存储在栈中。
类的成员变量在不同对象中各不相同,都有自己的存储空间(成员变量在堆中的对象中),基本类型和引用类型的成员变量都在这个对象的空间中,作为一个整体存储在堆。而类的方法却是该类的所有对象共享的,只有一套,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。
(7)内存中的执行过程

对指针和引用的概念有了一定的认识之后,下面就来看看链表的概念。我们知道,链表是由一系列节点构成的, 每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(“links”), 使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
在这里插入图片描述

			链表节点模型图

我们看到,在链表的节点模型图中,包括了两个基本成员属性,其中,data属性用于存储元素,nextNode用于指向下一个节点,因此nextNode存储的是下一个节点对象的地址。我们可以建立一个Node类来表示上面的节点模型:
package com.yzh.maven.main;
public class Node {
private Object data;
private Node nextNode;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Node getNextNode() {
return nextNode;
}
public void setNextNode(Node nextNode) {
this.nextNode = nextNode;
}
}

2.7.4.1单链表的数据结构图
根据概念,单链表(或单向链表)的结构如下图所示。

单向链表只可向一个方向遍历,一般查找一个节点的时候需要从第一个节点开始每次访问下一个节点,一直访问到需要的位置。而插入一个节点,对于单向链表,我们只提供在链表头插入,只需要将当前插入的节点设置为头节点,next指向原头节点即可。我们用代码模拟一下这个过程:
package com.yzh.maven.main;
public class CollectionTest7 {
public static void main(String[] args) {
Node node1 = new Node();
Node node2 = new Node();
Node node3 = new Node();
node1.setData(“1”);
//指向下一个节点node2
node1.setNextNode(node2);
node2.setData(“2”);
//指向下一个节点node3
node2.setNextNode(node3);
//指向下最后一个节点,为null
node3.setNextNode(null);
}
}

当然,取出单链表的所有节点的数据就更加简单,先获取根节点,循环判断是否为最后一个节点(最后一个节点为null),然后打印节点的数据,再把当前节点指向下一个节点。
package com.yzh.maven.main;
public class CollectionTest7 {
public static void main(String[] args) {
Node node1 = new Node();
Node node2 = new Node();
Node node3 = new Node();
node1.setData(“1”);
//指向下一个节点node2
node1.setNextNode(node2);
node2.setData(“2”);
//指向下一个节点node3
node2.setNextNode(node3);
node3.setData(“3”);
//指向下最后一个节点,为null
node3.setNextNode(null);

	Node current = node1;
	while(null != current){
		System.out.print(current.getData()+"->");
		current = current.getNextNode();
	}
}

}
/**output:
1->2->3->
*/

单链表是链表中结构最简单的。一个单链表的节点(Node)分为两个部分,第一个部分(data)保存或者显示关于节点的信息(需要存储的数据),另一个部分存储下一个节点的地址。最后一个节点存储地址的部分指向空值。
//从根节点开始取数据
Node root = new Node(“根节点”);
Node n1 = new Node(“车厢1”);
Node n2 = new Node(“车厢2”);
Node n3 = new Node(“车厢3”);

//取出数据
Node current = root;
while(current != null){
System.out.println(current.getData());
current = current.getNext();
}

我们知道,单链表是由一个一个节点组成的,每一个节点的数据域存储着节点的数据,指针域存储着下一个节点的地址,也就是指向下一个节点,由于是指向下一个节点,在java中指向的操作符号是”=”,因此指针域的数据类型为节点类型本身。我们这样来理解,在面向对象思想中,可以将节点抽象成一个类,这个类如下所示:
package com.yzh.maven.main;
public class Node {
public Object data;
public Node nextNode;
}
现在我们来组装一个链表:
Node node1 = new Node();
node1.data=“第一个节点”;
//node1的指针域要想指向下一个节点,必须下一个节点存在;
Node node2 = new Node();
//node1的指针域指向下一个节点,java中的指向用“=”表示,因此节点的指针域的类型是Node
node1.nextNode = node2;
node2.data=“第二个节点”;
//此时我们获取第二个节点的数据有两种方式:直接通过node2.data来获取,也可以通过第一个节点的指针域指向第二个节点这层关系来获取第二个节点的数据部分
System.out.println(node2.data);
System.out.println(node1.nextNode.data);
System.out.println(node2.data == node1.nextNode.data);//true,指向同一个对象,也就是拥有了同一个成员属性,它们都可以操作这个成员属性,且都会生效,相当于就是本身操作的这个成员属性(成员属性存储在堆区),所以引用一致
System.out.println(System.identityHashCode(node2.data));
System.out.println(System.identityHashCode(node1.nextNode.data));
//output:1079018694
//1079018694
Node node3 = new Node();
node3.data=“第三个节点”;
node2.nextNode=node3;
如何来获取这个链表的所有节点的数据呢?我们来分析一下,获取第一个的数据可以这样获取:
node1.data//通过头结点.data属性来获取
那么,通过第二个节点.data就可以获取到第二个节点的数据,同时,由于头节点指向第二个节点,因此,我们可以根据头节点与第二个节点的关系来获取第二个节点的数据;这个表达式如右所示:node1.nextNode.data,同理,获取第三个节点的数据的表达式为:node1.nextNode.nextNode.data。因此,我们可以通过传入头节点就可以遍历该单链表所有的数据。下面是遍历该单链表所有节点数据域数据的策略:
public static void showNodeData(Node node){
if(null == node){
return;
}else{
System.out.print(node.data+"->");
//传入下一个节点进行递归
showNodeData(node.nextNode);
}
}
/**output:
第一个节点->第二个节点->第三个节点->
*/

对于添加节点,我们也可以设计一个方法,如下所示:
//第一个参数是当前节点的数据,第二个参数是当前节点,第三个参数是下一个节点
public static void add(Object data,Node currentNode,Node nextNode){
if(null == data){
return;
}else{
//为当前节点的数据域添加数据
currentNode.data = data;
//当前节点的指针域指向下一个节点
currentNode.nextNode = nextNode;
}
}
测试:
Node node_1 = new Node();
Node node_2 = new Node();
Node node_3 = new Node();
Node node_4 = new Node();
add(“第一个节点”,node_1,node_2);
add(“第二个节点”,node_2,node_3);
add(“第三个节点”,node_3,node_4);
//最后一个节点为null
add(“第四个节点”,node_4,null);
//传入头节点,遍历该链表
showNodeData(node_1);
/**output:
第一个节点->第二个节点->第三个节点->第四个节点->
*/

上面的程序中,还存在需要改进的地方,在上面的Node类中,我们看到,该类的属性都是公共属性,这是不安全的,因此需要将其私有化,并提供get和set公共方法,外部类通过set和get方法对Node的私有化属性进行操作:
package com.yzh.maven.entity;
/**

  • @className Node.java

  • @description 单链表的节点模型

  • @author yzh

  • @param

  • @date 2019-01-27
    */
    public class Node {
    /*节点的数据域/
    private T data;
    /*节点的指针域/
    private Node next;
    public T getData() {
    return data;
    }
    public void setData(T data) {
    this.data = data;
    }
    public Node getNext() {
    return next;
    }
    public void setNext(Node next) {
    this.next = next;
    }
    }
    然后我们再来对以上的单链表进行调整:
    package com.yzh.maven.entity;
    public class MyLinkedListTest {
    public static void main(String[] args) {
    Node headNode = new Node();
    Node node2 = new Node();
    Node node3 = new Node();
    Node node4 = new Node();
    add(“头节点”,headNode,node2);
    add(“第二个节点”,node2,node3);
    add(“第三个节点”,node3,node4);
    add(“第四个节点”,node4,null);
    //传入头节点,获取所有节点信息
    showAllNodes(headNode);
    }

    /**

    • @functionName add
    • @description 为链表的当前节点添加数据和指针
    • @param data 为当前节点添加的数据
    • @param currentNode 当前节点
    • @param nextNode 下一个节点
    • @author yzh
    • @date 2019-01-27
      */
      public static void add(T data,Node currentNode,Node nextNode){
      if(data == null){
      return;
      }else{
      //为当前节点添加数据
      currentNode.setData(data);
      //当前节点指向下一个节点
      currentNode.setNext(nextNode);
      }
      }

    /**

    • @functionName showAllNodes
    • @description 获取单链表的所有节点数据域的数据
    • @param headNode 头节点
    • @author yzh
    • @date 2019-01-27
      */
      public static void showAllNodes(Node headNode){
      if(null == headNode){
      return;
      }else{
      System.out.print(headNode.getData()+"->");
      //让头节点指向下一个节点
      headNode = headNode.getNext();
      //递归
      showAllNodes(headNode);
      }
      }
      }
      /**output:
      头节点->第二个节点->第三个节点->第四个节点->
      */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值