数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)

Posted Android研究院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)相关的知识,希望对你有一定的参考价值。

数据结构之线性表


在开始数据结构前,先了解什么是数据结构?数据结构 + 算法 = 程序

数据结构的定义

数据结构是对在计算机内存中的数据的一种安排。也可以理解为对计算机运算的数据单元的一个抽象。也可以理解为数据结构是指在计算机内存空间中或磁盘中的组织形式。


数据结构的定义描述的有些抽象,举个栗子:

比如:美女和野兽,抽象的事物表示美女:头发长 前凸后翘。。。 可以表示为一个数据单元,野兽也是一个数据单元。

我们可以这样理解,数据结构是描述个体的数据集合,包含两者的关系,数据与数据之前的关系,逻辑的结构。

比如:在人机对弈中,棋盘、棋子、人 三者的关系,棋盘存储起来  棋子是个单独的数据 人是个对象 三者之间的关系,错综复杂的关系组合起来就是数据结构。

数据结构的逻辑结构

1. 集合结构

2. 线性结构

3. 树形结构

4. 图形结构


数据结构的存储结构

1. 表

2. 堆栈

3. 队列

4. 数据

5. 树

6. 二叉树

7. 图


了解了数据结构的基本内容,我们下面开始正题。


线性表

1. 顺序存储结构

2. 链式存储结构


顺序存储结构


顺序存储结构可以理解为:

种菜,比如种萝卜,一个萝卜一个坑,从第一个一直按顺序种到最后一个。

再比如我们去银行办理业务,都要先取号排队,一个号都对应着一个人。

前后相关联的


如下图所示:


图1



a1是a2的前驱,ai+1 是ai的后继,a1没有前驱,an没有后继

n为线性表的长度 ,若n==0时,线性表为空表.


顺序表的类模型:

class Array {   arrays[40];   int size; }


顺序表用数组保存一系列的数据。


顺序表的特征


删除操作:

数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)


顺序表删除



从上图中我们可以看出,当中间部位离去一个后,就会将该位置的后面的所有节点向前移一位。这是顺序表的删除操作。


中间插入操作:

数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)


顺序表插入



如上图所示,我们将ax 插入到 a2 - a3 之间,就需要将 a2 之后的所有数据向后移动一位。


尾部插入:将非常简单直接在尾部插入就可以了。

优点: 尾插效率高,支持随机访问。 缺点: 中间插入和删除效率低。 应用: ArrayList。 ArrayList 的底层实现便是对数组的操作。


下面我们就分析一下 ArrayList 的源码


ArrayList 源码分析


ArrayList 继承结构开始分析

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable


1. ArrayList  是继承于AbstractList。


AbstractList<E> extends AbstractCollection<E> implements List<E> AbstractList 继承自AbstractCollection 抽象类,实现了list 接口,是ArrayList和AbstractSequentiaList 的父类,它实现了list 的一些位置相关的操作 (add,remove,get,set) ,是第一个实现随机访问的集合类,但不支持添加和替换,后续我们在分析AbstractList 源码。


2. ArrayList 实现list 接口能对它进行队列操作

3. ArrayList 实现了Cloneable接口,覆盖了函数clone()

4. ArrayList 实现了java.io.Serializable 接口,支持序列化,能通过序列化传输数据

5. ArrayList 实现了RandomAccess接口是List 实现所使用的标记接口,用来表明其支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。在对List特别的遍历算法中,要尽量来判断是属于RandomAccess(如ArrayList)还是SequenceAccess(如LinkedList),因为适合RandomAccess List的遍历算法,用在SequenceAccess List上就差别很大。


ArrayList 属性

关键字transient 标识的字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。

我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。

然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。

总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。


/**    *       ArrayList的元素存储在其中的数组缓冲区。            ArrayList的容量是这个数组缓冲区的长度。当添加第一个元素时,任何具有elementData == EMPTY_ELEMENTDATA的空ArrayList将展开为DEFAULT_CAPACITY。 私有包允许从java.util.Collections访问。    */   transient Object[] elementData;   /**    * The size of the ArrayList (the number of elements it contains).    *    * @serial    */   private int size;       /**    * Default initial capacity.    */   private static final int DEFAULT_CAPACITY = 10;   /**    * Shared empty array instance used for empty instances.    */   private static final Object[] EMPTY_ELEMENTDATA = {};

ArrayList 就是对数组进行 添加 删除 修改操作的,默认初始的容量 DEFAULT_CAPACITY = 10


ArrayList 的构造函数

// 定义容量 也就是数组的大小 public ArrayList(int initialCapacity) {       super();       // 若传入的值小于0 抛出异常       if (initialCapacity < 0)           throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);       //初始化数组       this.elementData = new Object[initialCapacity];   }   /**    * 默认构造函数 初始化一个空的数组    */   public ArrayList() {       super();       // private static final Object[] EMPTY_ELEMENTDATA = {};       this.elementData = EMPTY_ELEMENTDATA;   }   /**    * 构造一个含有元素的列表    *    * @param c the collection whose elements are to be placed into this list    * @throws NullPointerException if the specified collection is null    */   public ArrayList(Collection<? extends E> c) {       // 初始化含有元素的数组       elementData = c.toArray();       // 数组的大小       size = elementData.length;       // c.toArray might (incorrectly) not return Object[] (see 6260652)       if (elementData.getClass() != Object[].class)           elementData = Arrays.copyOf(elementData, size, Object[].class);   }


ArrayList 添加元素


尾部添加


public boolean add(E e) {       ensureCapacityInternal(size + 1);  // Increments modCount!!       //进行完数组的自增长后才向数组中添加元素       elementData[size++] = e;       return true;   }   private void ensureCapacityInternal(int minCapacity) {       //如果elementData 是个空数组 没有元素       if (elementData == EMPTY_ELEMENTDATA) {           //minCapacity 取DEFAULT_CAPACITY 和 minCapacity 的最大值           minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);       }       ensureExplicitCapacity(minCapacity);   }   // 确保显式容量   private void ensureExplicitCapacity(int minCapacity) {       modCount++;       // overflow-conscious code minCapacity 必须大于数组的长度 才可自增长       if (minCapacity - elementData.length > 0)           grow(minCapacity);   }    // 实现数组的自增长    private void grow(int minCapacity) {       // overflow-conscious code 添加元素之前的数组长度       int oldCapacity = elementData.length;       // 新的数组长度       int newCapacity = oldCapacity + (oldCapacity >> 1);       // 若新的数组长度 小于 最小增长的数组长度,则 newCapacity = minCapacity       if (newCapacity - minCapacity < 0)           newCapacity = minCapacity;       //数组长度达到最大值           if (newCapacity - MAX_ARRAY_SIZE > 0)       //数组的最大长度           newCapacity = hugeCapacity(minCapacity);       // minCapacity is usually close to size, so this is a win:       // 对数组进行copy 扩大数组的长度       elementData = Arrays.copyOf(elementData, newCapacity);   }      private static int hugeCapacity(int minCapacity) {       if (minCapacity < 0) // overflow           throw new OutOfMemoryError();       return (minCapacity > MAX_ARRAY_SIZE) ?           Integer.MAX_VALUE :           MAX_ARRAY_SIZE;   }


中间插入 元素


public void add(int index, E element) {       if (index > size || index < 0)           throw new IndexOutOfBoundsException(outOfBoundsMsg(index));       ensureCapacityInternal(size + 1);  // Increments modCount!!       // 将插入元素 之后的数组后移一位 其实就是对数组的copy           src:源数组;           srcPos:源数组要复制的起始位置;           dest:目的数组;             destPos:目的数组放置的起始位置;    l           ength:复制的长度。       System.arraycopy(elementData, index, elementData, index + 1,                        size - index);      // 向数组中添加元素       elementData[index] = element;       size++;   }


> 不管是尾部插入 还是中间插入 都会对数组进行copy 效率不高。


查找元素

非常简单并且效率 非常高


public E get(int index) { if (index >= size)           throw new IndexOutOfBoundsException(outOfBoundsMsg(index));       return (E) elementData[index];   }

替换元素


非常简单并且效率 非常高

public E set(int index, E element) {       if (index >= size)           throw new IndexOutOfBoundsException(outOfBoundsMsg(index));       E oldValue = (E) elementData[index];       elementData[index] = element;       return oldValue;   }



删除元素


删除某个位置的元素 效率低

``` public E remove(int index) {       // 删除元素的位置必须大于或等于数组的大小       if (index >= size)           throw new IndexOutOfBoundsException(outOfBoundsMsg(index));       modCount++;       // 获取要删除的元素       E oldValue = (E) elementData[index];       //数据移动       int numMoved = size - index - 1;       // 若删除的不是最后一个       if (numMoved > 0)           //对数组进行copy           src:源数组;           srcPos:源数组要复制的起始位置;           dest:目的数组;             destPos:目的数组放置的起始位置;    l           ength:复制的长度。           System.arraycopy(elementData, index+1, elementData, index,                            numMoved);       // size - 1 前一位置空                           elementData[--size] = null; // clear to let GC do its work       // 返回删除的元素       return oldValue;   } ```


同理 删除元素 效率低

``` public boolean remove(Object o) {       if (o == null) {           for (int index = 0; index < size; index++)               if (elementData[index] == null) {                   fastRemove(index);                   return true;               }       } else {           for (int index = 0; index < size; index++)               // 找到这个位置的元素               if (o.equals(elementData[index])) {                   fastRemove(index);                   return true;               }       }       return false;   }   /*    * Private remove method that skips bounds checking and does not    * return the value removed.    */   private void fastRemove(int index) {       modCount++;       int numMoved = size - index - 1;       if (numMoved > 0)           System.arraycopy(elementData, index+1, elementData, index,                            numMoved);       elementData[--size] = null; // clear to let GC do its work   } ```

以上就是ArrayList 的核心方法,由此我们可以得出结论,顺序表最大的优点支持随机访问效率 很快,而最大的缺点就是 当数据量很大时,如果进行频繁的 删除 和 中间插入操作 将会非常慢 效率很低。在项目中要谨慎使用ArrayList


链式存储结构


定义: 线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组数据 元素可以是连续的,也可以是不连续的。


分类: 1: 单链表 2: 单循环链表 3: 双链表 4: 双向循环链表



链表的模型类

``` class Node{   Object data;   Node next; } ```


单链表


数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)

如图所示: 每个节点都有一个数据域P->data 一个指针域 P-> next -> data 指针域指向的当前节点的下一个节点。


单链表 增加元素 --> 中间插入

数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)

如图所示,可以看出,如果我们在中间插入一个元素,只需要将 插入节点的 前一个节点的指针域 之前要插入的节点,而插入节点的指针域 之前 后一个指针域,这样的插入 比ArrayList 的插入效率要高很多。


同理如果需要尾部插入:只需要将最后一个节点的指针域 指向插入的节点。


单链表的删除操作

数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)

如图所示: 如果要删除一个节点,只需要将 删除节点 的前一个节点的指针域 指向 删除节点的后一个节点。 这样删除要比 ArrayList的 删除效率要高很多。


单链表的应用: 优点:头插,中间插,删除效率高。 缺点:不支持随机访问 (查找要进行循环的方式查找 效率低) 应用场景: MessageQueue


双链表

--------------------------

数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)


如图所示:  明白上述的单链表,那么双链表 就很好理解了,同理 双链表 比 单链表 多了一个指针域,这个指针域指向的是前一个节点。


双链表的插入操作

我们定义节点两个指针域:per 指向前一个节点;next 指向后一个节点。 插入操作:将要插入的位置的 a节点的 next 指向 插入的c 节点;c 节点的per 指向a节点;c节点的next指向b节点;b节点的per 指向c节点。这样就完成了一个节点的插入操作。

双链表的删除操作



以上是关于数据结构-线性表(顺序表与链表的基本知识 以及ArrayList 源码分析)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构 顺序表与链表 四部曲总汇

顺序表与链表的基本操作

《数据结构》顺序表与链表

顺序表与链表

链表和顺序表的一些区别

作业三(第四周)