目录
前言
一、线性表概述
二、顺序存储结构
三、链式存储结构
3.1 单链表
3.2 循环链表
3.3 双向链表
四、顺序和链式对比
编程的本质就是对数据的处理。由于实际业务的数据之间存在复杂的逻辑关系,应用程序则需要分析这些数据的逻辑结构,并采用合适的物理结构来存储这些数据,并以此为基础对这些数据进行响应的操作。
从数据的逻辑结构来分,数据元素之前存在的关联关系被称为数据的逻辑结构。归纳起来,大致有一下4类基本的逻辑结构:
计算机在物理磁盘上通常有2种物理存储结构:
对于常用数据结构,可以将其简单地分为线性结构和非线性结构,其中线性结构主要是线性表,而非线性结构则主要是树和图。
线性表(Linear List)是由n(n≥0)个数据元素(节点)a1,a2,a3,…,an组成的有限序列,是线性结构中最常用而最简单的一种数据结构。线性表中每个元素必须具有相同的结构(即拥有相同的数据项)。
对于一个非空、有限的线性表而言,它总具有如下基本特征:
线性表的顺序存储结构是指用一组地址连续的存储单元依次存放线性表的元素。为了使用顺序结构实现线性表,程序通常会采用数组来保存线性表中的数据元素。
当程序采用顺序存储结构来实现线性表时,线性表中相邻元素两个元素ai和ai+1对应的存储地址loc(a1)和loc(ai+1)也是相邻的。
换句话来说,顺序结构线性表中数据元素的物理关系和逻辑关系是一致的,线性表中数据元素的存储地址可按如下公式计算。
loc(ai)=loc(a0)+i*b(0 < i < n)
上面公式中b代表每个数据元素的存储单元。从上面公式可以看出:程序获取线性表中每个元素的存储起始地址的时间相同,读取表中数据元素的时间也相同。而且顺序表中每个元素都可随机存取,因此顺序存储的线性表是一种随机存取的存储结构。
public class SequenceList {
private final int DEFAULT_SIZE = 16;
//数组的容量
private int capacity;
//定义一个数组用于保存顺序线性表的元素
private Object[] elementData;
//保存顺序表中元素的当前个数
private int size;
//以默认数组长度创建空顺序线性表
public SequenceList(){
capacity = DEFAULT_SIZE;
elementData = new Object[DEFAULT_SIZE];
}
//以一个初始化元素创建
public SequenceList(T element) {
this();
elementData[0] = element;
size++;
}
/**
* 以指定长度创建
* @param element
* @param initSize
*/
public SequenceList(T element, int initSize) {
capacity = 1;
while (capacity < initSize) {
capacity <<= 1;
}
elementData = new Object[capacity];
elementData[0] = element;
size++;
}
public int length() {
return size;
}
//根据索引获取元素
public T get(int i) {
if (i < 0 || i > size - 1) {
throw new IndexOutOfBoundsException();
}
return (T) elementData[i];
}
//返回元素的位置
public int locate(T element) {
for (int i = 0; i < size; i++) {
if (elementData[i].equals(element)) {
return i;
}
}
return -1;
}
//指定位置插入元素
public void insert(T element, int index) {
if (index < 0) {
throw new IndexOutOfBoundsException();
}
ensureCapacity(index);
if (size - index > 0) {
System.arraycopy(elementData, index, elementData, index + 1, size - index);
}
elementData[index] = element;
size++;
}
//判断数组的长度,不够的话以2倍扩容
private void ensureCapacity(int minCapacity) {
if (minCapacity >= capacity) {
while (capacity <= minCapacity) {
capacity <<= 1;
}
elementData = Arrays.copyOf(elementData, capacity);
}
}
//顺序添加元素
public void add(T element) {
if (size < capacity) {
elementData[size] = element;
size ++;
} else {
insert(element, size);
}
}
//删除指定位置元素
public T delete(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException();
}
T oldValue = (T) elementData[index];
int movNum = size - index - 1;
if (movNum > 0) {
System.arraycopy(elementData, index + 1, elementData, index, movNum);
}
elementData[--size] = null;
return oldValue;
}
//删除最后一个元素
public T remove() {
return delete(size - 1);
}
//判断是否为空
public boolean empty() {
return size == 0;
}
//清空线性表
public void clear() {
Arrays.fill(elementData, null);
size = 0;
}
public String toString() {
if (size == 0) {
return "[]";
} else {
StringBuilder stringBuilder = new StringBuilder("[");
for (int i = 0; i < size; i++) {
stringBuilder.append(elementData[i].toString() + ",");
}
int len = stringBuilder.length();
return stringBuilder.delete(len - 1, len).append("]").toString();
}
}
}
链式存储结构的线性表(也被简称为链表)将采用一组地址任意的存储单元存放线性表中的数据元素。链式结构的线性表不会按线性的逻辑顺序来保存数据元素,它需要在每一个数据元素里保存一个引用下一个数据元素的引用(或者叫指针)。
对于链式存储结构的顺序表而言,它的每个节点都必须包含数据元素本身和一或两个用来引用上一个/下一个节点的引用。也就是说,有如下公式。
节点 =数据元素+引用下一个节点的引用+引用上一个节点的引用
链表是多个相互引用的节点的集合,整个链表总是从头节点开始,然后依次向后指向每个节点。空链表就是头节点为null的链表。
单链表指的是每个节点只保留一个引用,该引用指向当前节点的下一个节点,没有引用指向头节点,尾节点的next引用为null。
简历单链表有两种方式:
public class LinkList {
//定义一个内部类Node,Node实例代表链表的节点
private class Node{
//节点数据
private T data;
//下个节点的引用
private Node next;
public Node(){
}
public Node(T data, Node next) {
this.data = data;
this.next = next;
}
}
//链表的头节点
private Node header;
//链表的尾结点
private Node tail;
//链表包含的节点数
private int size;
public LinkList() {
//空链表的header和tail都为null
header = null;
tail = null;
}
public LinkList(T element) {
header = new Node(element, null);
tail = header;
size++;
}
//返回链表长度
public int length() {
return size;
}
//获取索引处的元素
public T get(int index) {
return getNodeByIndex(index).data;
}
//根据索引,获取指定位置的节点
private Node getNodeByIndex(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException();
}
//从头开始
Node current = header;
for (int i = 0; i < size && current != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
//返回指定节点的索引
public int locate(T element) {
Node current = header;
for (int i = 0; i < size && current != null; i++, current = current.next) {
if (current.equals(element)){
return i;
}
}
return -1;
}
//插入下一个节点
public void insert(T element, int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException();
}
if (header == null) {
add(element);
} else {
if (index == 0) {
addAtHeader(element);
} else {
Node prev = getNodeByIndex(index -1);
prev.next = new Node(element, prev.next);
size++;
}
}
}
//采用头插法添加新节点
private void addAtHeader(T element) {
header = new Node(element, header);
if (tail == null) {
tail = header;
}
size++;
}
//采用尾插法添加新节点
private void add(T element) {
if (header == null) {
header = new Node(element, null);
tail = header;
} else {
Node newNode = new Node(element, null);
tail.next = newNode;
tail = newNode;
}
size++;
}
//删除指定位置的节点
public T delete(int index) {
if (index < 0 || index > size - 1) {
throw new IndexOutOfBoundsException();
}
Node del = null;
if (index == 0) {
del = header;
header = header.next;
} else {
Node prev = getNodeByIndex(index - 1);
del = prev.next;
prev.next = del.next;
del.next = null;
}
size--;
return (T) del;
}
//删除最后一个节点
public T remove() {
return delete(size - 1);
}
//判断是否为空
public boolean empty() {
return size == 0;
}
//清空
public void clear() {
header = null;
tail = null;
size = 0;
}
}
循环链表是一种首尾相接的链表。将单链表的尾节点next指针改为引用单链表header节点,这个单链表就成了循环链表。循环链表具有一个显著特征:从链表的任一节点出发均可找到表中其他所有节点,因此循环链表可以被视为“无头无尾”。
如果为每个节点保留两个引用prev和next,让prev指向当前节点的上一个节点,让next指向当前节点的下一个节点,此时的链表既可以向后依次访问每个节点,也可向前依次访问每个节点,这种形式的链表被称为双向链表。
双向链表是一种对称结构,它克服了单链表上指针单向性的缺点,其中每个节点既可向前引用,也可向后引用,这样可以更方便地插入、删除数据元素。
由于双向链表需要同时维护两个方向的指针,因此添加节点、删除节点时的指针维护成本更大;但双向链表具有两个方向的指针,因此可以向两个方向搜索节点,因此双向链表在搜索节点、删除指定索引处节点时具有较好的性能。
实现和单链表类似,节点多一个prev的指向。
顺序表 | 链表 | |
空间性能 | 顺序表的存储空间是静态分布的,需要一个长度固定的数组,因此总有部分数组元素被浪费。 | 链表的存储空间是动态分布的,因此不会有空间浪费。但由于链表需要额外的空间来为每个节点保存上个或者下个节点的引用(指针),因此会有一定空间的浪费。 |
时间性能 | 顺序表中元素的逻辑顺序与物理顺序保持一致,而且支持随机存取。因此顺序表在查找、读取时性能很好。 | 链表采用链式结构来保存元素,因此在插入、删除元素时的性能比较好。 |
从是原理以及实现上来看,由于顺序表的底层是采用数组实现,因此根据索引获取元素较为快速,并且由于在内存上是连续的空间,在遍历时也能较快;但是在元素的增加、删除上需要对其他元素要进行位移,因此性能不好。
链表的空间则是随机分布的,在结构上具有指向性,因此它的元素添加、删除较快,但是查询、遍历较慢。