链表的介绍
1.表中的结点呈现顺序排列,属于线性数据结构,就像一列火车一样,每一节车厢相当于表上的一个结点。
2.在表中当前结点之前的结点称为前驱,之后的结点为后继。
3.当表中没有元素时,也就是表元素个数为0,此时表为空表
表的数组实现
定义表
使用数组实现一个最简单的表结构,仅仅包含增加元素、修改元素、查找元素和删除元素,以及当数组容量不够时的扩容操作。在表中定义三个变量,表中数组的初始化大小,表中当前元素个数,和一个Object数组用于装载元素,因为Java数组是协变的,不支持泛型的使用,这里我们用Object初始化数组,之后强制类型转化。
private int occupation = 3;//定义表初始化长度为3
private int size = 0;//初始化0个元素
private Object[] elements = new Object[occupation];//Java数组是协变的,不支持泛型数组,这里用Object数组
增加元素
数组实现表的增加元素操作分两种,一种是增加元素到指定的索引位置,另一种是直接增加元素在表的结尾。关于扩容之后再说,假设数组容量充足的情况下,增加元素操作分两步进行:
- step1.索引之后的元素依次后移一位
- step2.在索引出插入数据,表的大小增加1
public E addElement(E ele){
while (size >= occupation){
resize();//扩容
}
elements[size++] = ele;//step1.结尾之后没有元素需要移动; step2.插入元素
return ele;
}
public E addElement(E ele, int index){
if (index >= occupation){
return null;//没有对应的结点位置
}
while (size >= occupation){
resize();//扩容
}
for (int i = size; i > index; i--){//step1.索引之后的元素依次后移一位
elements[i] = elements[i - 1];
}
elements[index] = ele;//step2.在索引出插入数据
size++;//step2.表的大小增加1
return ele;
}
修改元素
根据索引定位元素,修改数据。
public E modifyElement(E ele, int index){
if (index >= size || size == 0){
return null;
}
elements[index] = ele;
return ele;
}
查找元素
数组实现表结构分两种情况,第一种根据索引在数组中直接定位元素返回;第二种首先查找元素所在位置,查找成功返回结果。
public E getElement(int index){
if (index >= size || size == 0){
return null;
}
return (E)elements[index];//根据索引在数组中直接定位元素返回
}
public E getElement(E ele){
int index = -1;
for (int i = 0; i < size; i++){//查找元素所在位置
if (ele.equals((E)elements[i])){
index = i;
break;
}
}
if (-1 != index){//查找成功返回结果
return (E)elements[index];
}
return null;
}
删除元素
数组实现的表删除元素操作与增加元素操作刚好相反。增加操作是先后移再插入,删除操作则是先取出再前移。删除元素操作同样分两步进行:
- step0.如果函数传入元素,则首先需要定位元素所在索引
- step1.取出元素,表的大小减1
- step2.索引之后的元素依次前移一位
public E delElement(int index){
if (index >= size || size == 0){
return null;
}
Object ele = elements[index];//取出元素
size--;//表的大小减1
for (int i = index; i < size; i++){//索引之后的元素依次前移一位
elements[i] = elements[i + 1];
}
return (E)ele;
}
public E delElement(E ele){
int index = -1;
for (int i = 0; i < size; i++){//查找索引
if (ele.equals((E)elements[i])){
index = i;
break;
}
}
if (-1 != index){
return delElement(index);//取出元素,表的大小减1,索引之后的元素依次前移一位
}
return null;
}
扩容
扩容函数是在初始数组容量不够时插入元素所引发的操作,将原有的数组大小扩大,在这里我们把数组容量每次扩充到原来的2倍大小,并且使用Arrays.copyOf()函数把原有元素复制到新的扩容之后的数组中。
private void resize(){
occupation = occupation * 2;
elements = Arrays.copyOf(elements, occupation);
}
表的链表实现
定义结点
这里使用单向链表结构实现表,每个结点都具有指向下一个结点的引用next,最后一个结点next为null。并且链表具有头结点,头结点不代表任何元素,其next指向链表的第一个元素;为了遍历插入元素方便定义last指向链表的最后一个结点;定义size字段表示链表当前结点个数。
private int size = 0;
private MyLinkedListNode head = new MyLinkedListNode(null, null);//头指针,用于标示链表,不表示元素
private MyLinkedListNode last = null;
//链表结点的定义
private class MyLinkedListNode{
private E e;
private MyLinkedListNode next;
MyLinkedListNode(E e, MyLinkedListNode next) {
this.e = e;
this.next = next;
}
}
增加元素
因为存在last引用指向链表最后一个元素,因此在链表末尾插入元素很简单,在last后直接插入元素。唯一注意的就是空表是需要直接操作head头结点,并且赋值last引用。
public E addElement(E ele){
if (null == head.next){//当链表为空时,在头结点之后直接插入元素
head.next = new MyLinkedListNode(ele, null);
last = head.next;//更新last引用指向链表最后一个元素
size++;//链表大小加1
return ele;
}
last.next = new MyLinkedListNode(ele, null);//如果非空链表,在last后直接插入元素,即链表的末尾
last = last.next;//last更新
size++;//链表大小加1
return ele;
}
与数组表示不同之处在于链表不能通过索引直接定位要插入的位置
- step1.需要根据索引index先定位到将要插入到地方
- step2.再操作链表的next引用。
public E addElement(int index, E ele){
if (index > size){//index可以等于size,表示将元素插入到链表的末尾
return null;
}
MyLinkedListNode f = head;
for (int i = 0; i < index; i++){//step1.根据索引index先定位到将要插入到地方
f = f.next;
}//循环结束f为要插入元素的前一个位置,即index-1
MyLinkedListNode cur = new MyLinkedListNode(ele, null);//生成新结点
cur.next = f.next;//新结点next指向f的next
f.next = cur;//f结点next指向新结点,这样新结点就已经插入到index位置上
size++;//链表大小加1
return ele;
}
修改元素
定位结点与之前增加操作不同,增加操作是找到索引的前一个位置(index - 1),而修改需要定位索引位置(index)
- step1.定位索引index的结点
- step2.修改结点的值
public E modifyElement(int index, E ele){
if (index >= size){
return null;
}
MyLinkedListNode f = head;
for (int i = 0; i < index + 1; i++){//定位索引index的结点
f = f.next;
}//循环结束后,f结点就是索引index的位置
f.e = ele;//修改结点的值
return ele;
}
查找元素
链表根据索引查找元素从头节点往后递增遍历元素直到到达索引index位置
public E getElement(int index){
if (index >= size){
return null;
}
MyLinkedListNode f = head;
for (int i = 0; i < index + 1; i++){//链表查找元素从头节点往后递增遍历元素
f = f.next;
}//循环结束到达索引index位置
return f.e;
}
链表根据元素查找,从头节点往后遍历所有结点直到找到为止,如果最后没有找到返回null。
public E getElement(E ele){
MyLinkedListNode f = head;
for (int i = 0; i < size; i++){//从头节点往后遍历所有结点直到找到为止
f = f.next;
if (ele.equals(f.e)){
return ele;
}
}
return null;//如果最后没有找到返回null
}
删除元素
根据索引删除元素
- step1.定位元素前一个索引位置(index-1),即删除结点的前驱
- step2.操作指针,将删除结点前驱next指针指向删除结点的后继
public E delElement(int index){
if (index >= size){
return null;
}
MyLinkedListNode f = head;
for (int i = 0; i < index; i++){//定位删除结点的前驱
f = f.next;
}//循环结束f为删除结点的前驱
MyLinkedListNode del = f.next;
f.next = f.next.next;//操作指针,将删除结点前驱指针指向删除结点的后继
size--;
return del.e;
}
根据元素直接删除逻辑中,我们添加pre引用用于在遍历中时刻指向f引用的前驱,pre和f依次后移遍历链表的结点直到f找到要删除的结点为止,或者f为null时证明已经遍历到链表结尾没有找到元素。如果找到将要删除的元素,pre引用的next指向f.next指向的结点,f.next置null,这样删除的结点f就被删除了。
public E delElement(E ele){
MyLinkedListNode pre = head;//pre引用用于在遍历中时刻指向f引用的前驱
MyLinkedListNode f = head.next;
for (int i = 0; i < size; i++){//pre和f依次后移遍历链表的结点
if (ele.equals(f.e)){//找到将要删除的元素
break;
}
f = f.next;
pre = pre.next;
}
if (null != f){//f不为null,找到将要删除的元素
pre.next = f.next;//pre引用的next指向f.next指向的结点
f.next = null;//f.next置null
size--;//链表大小减1
return f.e;
}
return null;//f为null时,证明遍历整个链表没有找到元素
}
总结
利用数组和单向链表简单地实现数据结构表,并且提供基本的增删改查操作,这些操作都是表的最基本的实现。但仅仅这些操作并不足以应对日常的生产需要,表结构的操作还应该包含更多,例如判断是否为空表,表的最大大小设定(在我们例子中可以向表中无限添加元素直到内存耗尽为止,如果有别有用心的人向表中恶意增加元素将导致我们系统崩溃)等等;在多线程环境中还需要考虑并发时数据的一致性等问题;单向链表实现具有局限性,如果寻找某个结点的前驱在没有特殊的辅助设计时需要从头再遍历一遍链表。所幸Java的API提供了表的实现ArrayList和LinkedList(双向链表实现),具有更完善的操作;java.util.concurrent提供多线程环境下的抽象数据结构。
相关算法例题
链表表示的两数相加
删除链表的倒数第n个结点
合并两个有序链表
合并k个有序链表
两两交换链表中的节点
K个一组翻转链表
链表每个节点向右循环移动 k 个位置