前言
一、单链表的定义
二、单链表的结构
三、单链表的实现
1.结点类的定义
2.定义单链表泛型类
3.单链表中的基本算法
1)以头插法建立表单
2)以尾插法建立表单
3)获取链表长度
4)将元素插入链表末尾
4)获取第i位的值
5)替换指定位的元素
6)获取第一个值为data的元素的序号
7)删除第i位的元素
8)将新的结点插入在特定结点后面
四、单链表的应用
1.合并链表问题
2.快慢指针法
总结
链表结构是线性表的一种结构。链表中的每一个结点不仅包括了其本身的信息,还设有结点包含了元素间的逻辑关系,即包含有后继结点或前驱结点的地址信息,这些含有逻辑关系结点称为指针成员。链表的优点在于便于修改。而链表分为单链表和双链表。单链表表示每一个结点只设有一个指针成员指向其后继结点,因此单链表只能单向遍历。
每一个结点只设置一个指向后继结点的指针成员的表单称为单链表。在单链表中,只能按顺序访问结点的后继结点。
单链表包含一个头节点head,用.next的方式连接后继结点,末尾元素的后继结点需设为null。如第一项数据为head.next。
public class LinkedNode { //将表示结点的类设置为泛型类
E data; //表示该结点中包含的数据
LinkedNode next; //指针成员,单链表中只含有一个指向后继结点的指针成员
public LinkedNode(){ //无参构造方法
next = null;
}
public LinkedNode(E data){ //在创建结点时传入该结点的数据
this.data = data;
next = null;
}
}
public class LinkedListClass { //将该类设为泛型类
LinkedNode head; //头结点对象
public LinkedListClass(){ //构造方法
head = new LinkedNode(); //实现头结点
head.next = null; //头节点的后继结点设为null
}
}
该构造方法使得我们在创建单链表对象时,得到的是一个只包含了头节点的空链表。
单链表的整体创建分为头插法和尾插法,头插法是将新结点dataNode插入到表头后,即head.next=dataNode。用该方法得到的链表跟原数组结构的顺序相反。
同理,将后续的元素逐个插入
代码如下
/**
* 以头插法将数组转换为链表结构
* @param arr: 数据数组
*/
public void createFromFirst(E[] arr){
LinkedNode dataNode; //创建变量表示要插入的元素
for (int i = 0; i < arr.length; i++) { //遍历数组获取每一位的元素
dataNode = new LinkedNode(arr[i]); //为该变量赋值,值为数组中的值
dataNode.next = head.next; //如步骤2
head.next = dataNode; //如步骤3
}
}
与头插法不同,尾插法是将结点插入在尾结点之前,得到的链表与原数组顺序相同。
在遍历插入数组元素后,指针依旧是指向尾元素,此时需将该元素的后继结点设为null。
/**
* 以尾插法建立链表结构
* @param arr: 数组数据
*/
public void createFromLast(E[] arr){
LinkedNode dataNode; //创建变量表示要插入的元素
LinkedNode pointer = head; //表示指针,指向尾元素(此时该链表为空链表,头节点即尾结点)
for (int i = 0; i < arr.length; i++) { //遍历数组获取每一位的元素
dataNode = new LinkedNode(arr[i]); //为该变量赋值,值为数组中的值
pointer.next = dataNode; //如步骤2
pointer = dataNode; //如步骤3
}
pointer.next = null; //将尾结点的后继结点设为null
}
/**
* 获取链表长度
* @return: 链表长度
*/
public int getSize(){
LinkedNode pointer = head; //初始化指针指向头节点
int size = 0; //定义变量size,初始值为0
while (pointer.next!=null){ //循环遍历每一项,当pointer的后继结点为null时,链表读到末尾
pointer = pointer.next; //将指针后移一位
size++; //size自增1
}
return size; //返回size的值
}
将元素插入末尾时,需先遍历链表将指针指到尾结点
/**
* 将元素加入链表末尾
* @param e:需加入的元素
*/
public void add(E e){
LinkedNode pointer = head; //初始化指针指向头节点
LinkedNode dataNode = new LinkedNode(e); //新建结点dataNode
while (pointer.next!=null){ //循环遍历每一项,当pointer的后继结点为null时,链表读到末尾
pointer=pointer.next; //将指针后移一位
} //遍历结束,指针指向尾结点
pointer.next = dataNode; //将dataNode插入尾结点后面
}
需要注意的是,此时无需将dataNode的后继结点设为null,因为结点在创建时的后继结点为null
/**
* 查找序号为i的元素
* @param i:需查找的序号为i的元素
* @return :指针所指向的值,即第i位的值
*/
public LinkedNode getI(int i){
LinkedNode pointer = head; //初始化指针指向头节点
for (int j = 0; j < i; j++) { //遍历链表直到第i位
pointer = pointer.next; //将指针后移一位
}
return pointer; //返回指针所指向的值,即第i位的值
}
/**
* 将指定位的元素替换
* @param i: 第i位
* @param data: 替换的数据
*/
public void setEle(int i,E data){
if(i<0 || i>getSize()){ //当i值小于0或大于链表长度时,抛出错误
throw new IllegalArgumentException("i不在有效范围内");
}
getI(i).data = data; //获取第i位的元素并将它的值换为传入的data值
}
/**
* 获取第一个值为e的元素序号,当未找到元素时返回-1
* @param data:要找到的值
* @return :返回该元素的序号,未找到时返回-1
*/
public int getNum(E data){
LinkedNode pointer = head; //初始化指针指向头节点
int num = 0; //定义变量num表示序号,初始化为0
while (pointer.next!=null && pointer.data!=data){ //遍历链表,当pointer.data等于需找到的值,或者遍历到末尾时,结束循环
num++;
pointer = pointer.next; //指针向后移一位
}
if(pointer==null){ //当pointer指到末尾,表示未找到元素,此时返回-1
return -1;
}
return num;
}
在单链表中,删除元素时需获取该元素的前一位元素(如元素a0),再通过a0.next=a0.next.next的方式删除元素。
代码如下
/**
* 删除第i位元素
* @param i
*/
public void delete(int i){
if(i<0 || i>getSize()){
throw new IllegalArgumentException("i不在有效范围内");
}
getI(i-1).next = getI(i-1).next.next;
}
与头插法所示图片相同,先将插入结点与被插入结点的后继结点相连,再将被插入结点与插入结点相连即可
/**
*
* 将data数据插入在position的后面
* @param data:需要插入的数据
* @param position:插入结点
*/
public void insert(E data,E position){
LinkedNode dataNode = new LinkedNode(data);
LinkedNode pointer = head;
while (pointer.next!=null){
pointer.next = pointer;
if (pointer.data==data){
dataNode.next=pointer.next; //将dataNode与pointer的后继结点相连
pointer.next=dataNode; //将dataNode作为pointer的后继结点
return;
}
}
System.out.println("未找到"+position+"数据");
}
需要注意的是,插入结点的两行代码顺序不能改变
存在两个整数单链表A和B,设计一个算法将A,B中的所有数据结点以(a0,b0,a1,b1,...) 的方式合并得到单链表C。这种问题的算法称为二路归并算法,其代码如下:
/**
* 将两个单链表以(a0,b0,a1,a1,...)的形式合并
* @param A
* @param B
* @return
*/
public static LinkedListClass combine(LinkedListClass A,LinkedListClass B){
LinkedListClass C = new LinkedListClass<>();
LinkedNode aPointer = A.head.next;
LinkedNode bPointer = B.head.next;
LinkedNode cPointer = C.head;
while (aPointer.next!=null && bPointer!=null){ //当A,B链表都未遍历完时,需轮流插入到链表B,故循环条件为A,B都存在元素
cPointer.next = aPointer; //由于有顺序要求(a0,b0,a1,b1),故采用尾插法是链表顺序与数组顺序一致
cPointer = aPointer;
aPointer = aPointer.next;
cPointer.next = bPointer;
cPointer = bPointer;
bPointer = bPointer.next;
}
cPointer.next = null; //将尾结点的后继结点设为null
if(aPointer!=null){
cPointer.next = aPointer; //将未遍历玩的链表整体接入链表C
}
if(bPointer!=null){
cPointer.next = aPointer;
}
return C;
}
有一个长度大于2的整数单链表,设计一个算法查找该链表的中位数,如(1,2,3,4)返回2。代码如下
/**
* 获取Integer类型链表的中间数
* @param linkedList:
* @return: 中间数
*/
public static int middle(LinkedListClass linkedList){
LinkedNode fastPointer = linkedList.head.next; //快指针
LinkedNode slowPointer = linkedList.head.next; //慢指针
while (fastPointer.next!=null && fastPointer.next.next!=null){ //快指针无论如何走的都比慢指针快,所以仅需要判断快指针即可,又由于快指针一次走两个结点,所以需要判断两个结点后是否为null
slowPointer = slowPointer.next; //慢指针走一个结点
fastPointer = fastPointer.next.next; //快指针走两个结点
}
return slowPointer.data;
}
单链表的难点在于指针的位置,我建议在写代码的时候用纸笔画个图,想清楚指针要怎么移动。