线性表的定义
线性表(List):零个或多个数据元素的有限序列。
数学定义:若将线性表记为( a 1 , a 2 , ⋯ , a i − 1 , a i , a i + 1 , ⋯ , a n a_{1}, a_{2},⋯,a_{i-1},a_{i},a_{i+1},⋯,a_{n} a1,a2,⋯,ai−1,ai,ai+1,⋯,an),则表中 a i − 1 a_{i-1} ai−1领先于 a i a_{i} ai, a i a_{i} ai领先于 a i + 1 a_{i+1} ai+1, 称 a i − 1 a_{i-1} ai−1是 a i a_{i} ai的直接前驱元素, a i + 1 a_{i+1} ai+1是 a i a_{i} ai的直接后继元素。当i=1,2,⋯,n-1时, a i a_{i} ai有且仅有一个直接后继,当i=2,3,⋯,n时, a i a_{i} ai有且仅有一个直接前驱。
案例
上图为一个线性表,其中长度为5,注意当一个线性表长度为0的时候,成为空表。
在复杂的线性表中,一个数据元素可以由若干个数据项组成。如上图,每种动物都有自己的特点,都有自己的数据项。
线性表的存储结构
顺序存储结构
线性表的顺序存储结构:用一段连续的地址存储单元,依次的把线性表的数据元素存储起来的的结构。
在Java中,ArrayList就是用到了这种结构:
先看声明的变量
//存储空间的初始分配量
private static final int DEFAULT_CAPACITY = 10;
//初始化存储空间,默认空表
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//当前数组,用来存储数据元素。说明ArrayList底层就是一个数组。
transient Object[] elementData;
//线性表当前长度或者数组当前长度
private int size;
线性表顺序存储结构的插入
/**
* 把element的数据元素插入到index的下标中
*/
public void add(int index, E element) {
//如果下标大于最大和长度或者下标小于0抛异常。
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//计算是否要扩容
ensureCapacityInternal(size + 1);
//将elementData数组的index的下表开始,把size - index个数据复制到elementData的index+1开始的下标上。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//用数组下标赋值,即插入数组
elementData[index] = element;
size++;//长度加一
}
线性表顺序存储结构的删除
/**
* 删除下标为index的元素,并返回移除的元素
*
*/
public E remove(int index) {
//如果下标大于长度,抛异常
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//计数器
modCount++;
//获取数组中下标为index的元素
E oldValue = (E) elementData[index];
//要copy的数组的长度
int numMoved = size - index - 1;
if (numMoved > 0)
//将elementData数组的index+1的下表开始,把numMoved个数据复制到elementData的index开始的下标上。
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//把最后的下标置为空
elementData[--size] = null; // clear to let GC do its work
//最后返回移除的数据
return oldValue;
}
线性表顺序存储结构的删除
//根据下标直接返回数组元素
public E get(int index) {
if (index < 0 || index >= this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
return (E) ArrayList.this.elementData[offset + index];
}
线性表顺序存储结构的优缺点:
优点 | 缺点 |
---|---|
1、无须为表中的逻辑关系增加额外的存储空间 | 1、插入与删除需要移动大量的元素 |
2、可以快速的插入与删除 | 2、当线性表长度变化较大的时候,难以确定存储空间的容量 |
3、造成存储空间的“碎片” |
链式存储结构
对数据元素 a i a_{i} ai来说,除 了有本身的数据域外,还需要存储一个直接指向后继的存储位置的指示器。这个指示器叫指针。
包含数据域跟指针的数据元素称为节点。
由n个节点连接成一个链表,称为线性表的链式存储结构,每个节点包含两个指针,一个指向前驱节点,一个指向后继节点的链表称为双向链表。
在Java中LinkList就是用到了这种结构:
节点
/**
* LinkedList<E> 中的内部类定义的节点
**/
private static class Node<E> {
E item;//数据域
LinkedList.Node<E> next;//指向上一个节点的指针
LinkedList.Node<E> prev;//指向下一个节点的指针
Node(LinkedList.Node<E> var1, E var2, LinkedList.Node<E> var3) {
this.item = var2;//数据域
this.next = var3;//指向后继节点的指针
this.prev = var1;//指向前驱节点的指针
}
}
双向链表定义
transient int size;//两边长度
transient LinkedList.Node<E> first;//链表的头节点
transient LinkedList.Node<E> last;//链表的尾节点
双向链表插入
/**
* 把数据var2 插入到 链表的var1位置上
**/
public void add(int var1, E var2) {
//检查插入的位置是否合适>0,并且<size
this.checkPositionIndex(var1);
if (var1 == this.size) {
//插入末尾
this.linkLast(var2);
} else {
//插入中间
this.linkBefore(var2, this.node(var1));
}
}
//要把var1的节点插入到var2后
void linkBefore(E var1, LinkedList.Node<E> var2) {
//获取var2的前一节点并赋值var3
LinkedList.Node var3 = var2.prev;
//创建数据域为var1的节点var4 ,并把指针var3作为前驱节点,var2作为后继节点
LinkedList.Node var4 = new LinkedList.Node(var3, var1, var2);
//var2的前驱节点为var4
var2.prev = var4;
//var3 为空,表示var2为头结点,则新节点var4就作为头结点
if (var3 == null) {
this.first = var4;
} else {
//var3的后继节点为var4
var3.next = var4;
}
//链表长度加1
++this.size;
//计数器加1
++this.modCount;
}
如下图
双向链表删除
//移除链表中为var1的元素,返回是否移除成功标识
public boolean remove(Object var1) {
LinkedList.Node var2;
//如果移除的元素为空
if (var1 == null) {
//从头结点开始遍历
for(var2 = this.first; var2 != null; var2 = var2.next) {
//数据域为空移除
if (var2.item == null) {
this.unlink(var2);
return true;
}
}
} else {
//从头结点开始遍历
for(var2 = this.first; var2 != null; var2 = var2.next) {
//数据域为空移除
if (var1.equals(var2.item)) {
//移除
this.unlink(var2);
return true;
}
}
}
return false;
}
//移除var1节点
E unlink(LinkedList.Node<E> var1) {
Object var2 = var1.item;//获取数据域
LinkedList.Node var3 = var1.next;//获取节点的后继节点
LinkedList.Node var4 = var1.prev;//获取前驱节点
//如果前驱节点为空,这表示var1就是头结点,这时要把var1移除的话,就把var1的后继节点var3 当做头结点。
if (var4 == null) {
this.first = var3;
} else {
var4.next = var3;//把原节点var1的前驱元素var4的后继节点不再指向var1而是指向var1的后继节点var3。
var1.prev = null;//把原节点var1的前驱节点置为空。
}
//如果后继节点为空,这表示var1就是尾结点,这时要把var1移除的话,就把var1的后继节点var4 当做尾结点。
if (var3 == null) {
this.last = var4;
} else {
var3.prev = var4;//把原节点var1的后继元素var3的前驱节点不再指向var1而是指向var1的前驱节点var4。
var1.next = null;//把原节点var1的后继节点置为空。
}
//把原节点var1的数据域置为空
var1.item = null;
--this.size;//长度减一
++this.modCount;//计数器加一
return var2;//返回原节点var1的数据域
}
如下图所示
线性表链式存储结构查找
//获取下表为var1的节点
public E get(int var1) {
//检查下标异常情况
this.checkElementIndex(var1);
//返回节点的数据域
return this.node(var1).item;
}
//根据下标查找节点
LinkedList.Node<E> node(int var1) {
LinkedList.Node var2;
int var3;
//二分法思想,下标在0到size/2之间则从链表头开始遍历
if (var1 < this.size >> 1) {
var2 = this.first;
for(var3 = 0; var3 < var1; ++var3) {
var2 = var2.next;
}
return var2;
} else {
var2 = this.last;
//二分法思想,下标在size/2到size之间则从链表尾开始遍历
for(var3 = this.size - 1; var3 > var1; --var3) {
var2 = var2.prev;
}
return var2;
}
}
线性表链式存储结构的优缺点:
优点 | 缺点 |
---|---|
在插入和删除的操作时,只需要修改指针,不需要移动其他元素,效率高 | 不需要提前分配空间,但查找元素需要遍历整个链表,效率低 |
总结
若线性表需要频繁查找,很少插入与删除的操作时,应该使用顺序存储结构(ArrayList)。若需要频繁插入或者删除时,应该考虑链式存储结构(LinkList)。
当线性表中的元素个数变化较大或者根本不知道多大时,最好使用链式存储结构,这样就不需要考虑存储空间的大小问题。
本文主要参考《大话数据结构》