链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
**每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 **
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
**优点 **使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
**不足: **但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
链表有很多种不同的类型:单向链表(线性链表),双向链表以及循环链表
**
- 线性表
- ** 线性表是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。线性表有两种存储方式,一种是顺序存储结构,另一种是链式存储结构。
顺序存储结构就是两个相邻的元素在内存中也是相邻的。这种存储方式的优点是查询的时间复杂度为O(1),通过首地址和偏移量就可以直接访问到某元素,关于查找的适配算法很多,最快可以达到O(logn)。缺点是插入和删除的时间复杂度最坏能达到O(n),如果你在第一个位置插入一个元素,你需要把数组的每一个元素向后移动一位,如果你在第一个位置删除一个元素,你需要把数组的每一个元素向前移动一位。还有一个缺点,就是当你不确定元素的数量时,你开的数组必须保证能够放下元素最大数量,遗憾的是如果实际数量比最大数量少很多时,你开的数组没有用到的内存就只能浪费掉了。
我们常用的数组就是一种典型的顺序存储结构,如图
链式存储结构就是两个相邻的元素在内存中可能不是相邻的,每一个元素都有一个指针域,指针域一般是存储着到下一个元素的指针。这种存储方式的优点是插入和删除的时间复杂度为O(1),不会浪费太多内存,添加元素的时候才会申请内存,删除元素会释放内存,。缺点是访问的时间复杂度最坏为O(n),关于查找的算法很少,一般只能遍历,这样时间复杂度也是线性(O(n))的了,频繁的申请和释放内存也会消耗时间。
顺序表的特性是随机读取,也就是访问一个元素的时间复杂度是O(1),链式表的特性是插入和删除的时间复杂度为O(1)。要根据实际情况去选取适合自己的存储结构。
链表就是链式存储的线性表。根据指针域的不同,链表分为单向链表、双向链表、循环链表等等。
**一、 单向链表(slist) ** 链表中最简单的一种是单向链表,每个元素包含两个域,值域和指针域,我们把这样的元素称之为节点。每个节点的指针域内有一个指针,指向下一个节点,而最后一个节点则指向一个空值。
如图就是一个单向链表。
一个单向链表的节点被分成两个部分。第一个部分保存或者显示关于节点的信息,第二个部分存储下一个节点的地址。单向链表只可向一个方向遍历。
代码实现
public class AppLinkList<T> {
private Node header;// 保存头结点
private Node tail;// 保存尾节点
private int size;// 保存已含有的节点数
public AppLinkList() {
header = null;
tail = null;
}
private class Node<T> { // 定义一个节点类
private T data;// 保存数据
private Node next;// 指向下个节点的引用
public Node() {}
// 初始化全部属性的构造器
public Node(T data, Node next) {
this.data = data;
this.next = next;
}
}
/**
* 已指定数据元素创建链表,只有一个元素
*
* @param element
*/
public AppLinkList(T element) {
header = new Node(element, null);
// 只有一个节点,header,tail都指向该节点
tail = header;
size++;
}
/**
* 获取指定索引处的元素
*
* @param index
* @return
*/
public T get(int index) {
return (T) this.getNodeByIndex(index).data;
}
/**
* 获取指定位置的节点元素
* @param index
* @return
*/
private Node getNodeByIndex(int index) {
if (index < 0 || index > size - 1) {
throw new IndexOutOfBoundsException("索引超出线性表范围");
}
Node current = header;// 从header开始遍历
for (int i = 0; i < size && current != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
/**
* 按值查找所在位置
* @param element
* @return
*/
public int locate(T element) {
Node current = header;
for (int i = 0; i < size && current != null; i++, current = current.next) {
if (current.data.equals(element)) {
return i;
}
}
return -1;
}
/**
* 指定位置插入元素
* @param element
* @param index
*/
public void insert(T element, int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引超出线性表范围");
}
// 如果是空链表
if (header == null) {
// add(element);
} else {
// 当index为0时,即在链表头处插入
if (0 == index) {
addAtHead(element);
}
}
}
/**
* 尾部插入元素
* @param element
*/
public void add(T element) {
// 如果链表是空的
if (header == null) {
header = new Node(element, null);
// 只有一个节点,headwe,tail都该指向该节点
tail = header;
} else {
Node newNode = new Node(element, null);// 创建新节点
tail.next = newNode;// 尾节点的next指向新节点
tail = newNode;// 将新节点作为尾节点
}
size++;
}
/**
* 头部插入
* @param element
*/
public void addAtHead(T element) {
// 创建新节点,让新节点的next指向header
// 并以新节点作为新的header
Node newNode = new Node(element, null);
newNode.next = header;
header = newNode;
// 若插入前是空表
if (tail == null) {
tail = header;
}
size++;
}
/**
* 删除指定索引出元素
*
* @param index
* @return
*/
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;// 将被删除节点的next引用置为空
}
size--;
return (T) del.data;
}
/**
* 删除最后一个元素
*
* @return
*/
public T remove() {
return delete(size - 1);
}
/**
* 判断链表是否为空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 清空线性表
*/
public void clear() {
// 将header,tail置为null
header = null;
tail = null;
size = 0;
}
public String toString() {
if (isEmpty()) {
return "[]";
} else {
StringBuilder sb = new StringBuilder("[");
for (Node current = header; current != null; current = current.next) {
sb.append(current.data.toString() + ", ");
}
int len = sb.length();
return sb.delete(len - 2, len).append("]").toString();
}
}
/**
* 返回链表的长度
*
* @return
*/
public int length() {
return size;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
// 测试构造函数
AppLinkList<String> list = new AppLinkList("测试");
System.out.println(list);
// 测试添加元素
list.add("hello");
list.add("word");
System.out.println(list);
// 在头部添加
list.addAtHead("五月");
System.out.println(list);
// 在指定位置添加
list.insert("测试", 2);
System.out.println(list);
// 获取指定位置处的元素
System.out.println("第2个元素是(从0开始计数):" + list.get(2));
// 返回元素索引
System.out.println("摩卡在的位置是:" + list.locate("摩卡"));
System.out.println("moka所在的位置:" + list.locate("moka"));
// 获取长度
System.out.println("当前线性表的长度:" + list.length());
// 判断是否为空
System.out.println(list.isEmpty());
// 删除最后一个元素
list.remove();
System.out.println("调用remove()后:" + list);
// 获取长度
System.out.println("当前线性表的长度:" + list.length());
// 删除指定位置处元素
list.delete(3);
System.out.println("删除第4个元素后:" + list);
// 获取长度
System.out.println("当前线性表的长度:" + list.length());
// 清空
list.clear();
System.out.println(list);
// 判断是否为空
}
}
**二、 双向链表 ** 双向链表的指针域有两个指针,每个数据结点分别指向直接后继和直接前驱。单向链表只能从表头开始向后遍历,而双向链表不但可以从前向后遍历,也可以从后向前遍历。除了双向遍历的优点,双向链表的删除的时间复杂度会降为O(1),因为直接通过目的指针就可以找到前驱节点,单向链表得从表头开始遍历寻找前驱节点。缺点是每个节点多了一个指针的空间开销。
如图就是一个双向链表
**三、 循环链表 **
循环链表就是让链表的最后一个节点指向第一个节点,这样就形成了一个圆环,可以循环遍历。单向循环链表可以单向循环遍历,双向循环链表的头节点的指针也要指向最后一个节点,这样的可以双向循环遍历。
如图就是一个双向循环链表。