链表
1、前言
1.1、链表数据结构
链表种类还是比较繁多的,根据指针域可以分为单链表与双向链表,根据头尾是否连接可以分为循环链表与不循环链表,根据是否有头指针可以分为有头链表与无头链表。
大致排列组合就可以得到8种链表结构。
常用的是无头不循环单链表和有头双向循环链表,前者结构简单便于理解,后者结构复杂利于实用。
//无头单链表
public static class SingleLinkedList {
private int code;// 值
private SingleLinkedList next;// 下一个
public SingleLinkedList() {
}
public SingleLinkedList(int code, SingleLinkedList next) {
this.code = code;
this.next = next;
}
//省略get、set
public void write() {
SingleLinkedList node = this;
int sum = 0;
while (node != null && sum++ < 20) {
System.out.print(node.getCode() + ",");
node = node.getNext();
}
if (sum < 20) {
System.out.println();
} else {
System.out.println("......");
}
}
public int length() {
SingleLinkedList node = this;
int length = 0;
while (node != null) {
length++;
node = node.next;
}
return length;
}
}
// 有头双向循环链表
public static class DoubleLinkedList {
private int code;// 值
private DoubleLinkedList previous;// 上一个
private DoubleLinkedList next;// 下一个
public DoubleLinkedList() {
}
//省略get、set
public void write() {
System.out.print(this.getCode() + ",");
DoubleLinkedList node = this.next;
while (node != this) {
System.out.print(node.getCode() + ",");
node = node.getNext();
}
System.out.println();
}
public boolean insert(int newCode, int place) {
// 取余减少遍历次数
place = place % this.getCode();
DoubleLinkedList node = this;
while (place-- != 0) {
node = node.getNext();
}
DoubleLinkedList newNode = new DoubleLinkedList(newCode, node, node.getNext());
node.getNext().setPrevious(newNode);
node.setNext(newNode);
this.setCode(this.getCode() + 1);
System.out.print("insertDouble-Success:");
this.write();
return true;
}
}
1.2、优缺点与比较
和链表一起比较的一般是顺序表。
顺序表是连续的,新建时开辟一块大小固定的空间用于存放所有元素,需要扩容时再开辟一块相同大小的空间。
链表是不连续的,每次只开辟一块空间用于存放一个结点内容与下一个结点的地址,当有新结点时再开辟一块空间。
对于空间浪费:顺序表的空间浪费主要在于表内元素的不确定导致的开辟空间浪费,这个浪费是不确定的,最好情况是开辟与表内元素大小相同的空间,使存储密度为1.;链表则是有一个元素开辟一块空间,但存储元素时还需要存储下一个元素的位置。
对于碎片空间:碎片空间往往产生与内存开辟时,因为内存随机存储,有一些小的空间已经不能用于存储数据。自然需要多次开辟内存的链表会产生更多碎片空间。
对于数据操作:由于顺序表元素位置固定,在查找上就更快捷,同样的,在插入删除元素时就需要改变其他元素的位置;链表正好相反,在插入删除时只需改变前后元素的指针域,但查找时就需要遍历。
该部分内容参考自线性表之顺序表与单链表的区别及优缺点。
1.3、总结
增删多用链表,查找多用顺序表。
2、基本操作
特别说明:除2.3链表逆置部分代码外,其余方法均封装在对应类中。
2.1、新建链表
只需调用链表类的构造方法即可。
public static void main(String[] args) {
// 新建无头单链表
SingleLinkedList singleHead = new SingleLinkedList(0, null);
// 新建有头双向循环链表
DoubleLinkedList doubleHead = new DoubleLinkedList(1);
}
2.2、增删查
2.2.1、增
也就是插入链表,我这里实现了传入一个参数插入链表尾部和给定位置插入两种实现方法。
//无头单链表
public boolean insert(int newCode, int place) {
SingleLinkedList node = this;
while (node != null) {
if (place-- == 0) {
// 新建结点
SingleLinkedList newNode = new SingleLinkedList(newCode, node.getNext());
node.setNext(newNode);
System.out.print("insertSingle-Success:");
this.write();
return true;
} else {
node = node.getNext();
}
}
System.out.println("insertSingle-Fail:超出链表长度");
return false;
}
public boolean insertLast(int newCode) {
SingleLinkedList node = this;
while (node.next != null) {
node = node.getNext();
}
SingleLinkedList newNode = new SingleLinkedList(newCode, null);
node.setNext(newNode);
System.out.print("insertLastSingle-Success:");
this.write();
return true;
}
public boolean insertLast(SingleLinkedList newNode) {
SingleLinkedList node = this;
while (node.next != null) {
node = node.getNext();
}
node.setNext(newNode);
System.out.print("insertLastSingle-Success:");
this.write();
return true;
}
双向无头链表仅实现了固定位置插入
//有头双向循环链表
public boolean insert(int newCode, int place) {
// 取余减少遍历次数
place = place % this.getCode();
DoubleLinkedList node = this;
while (place-- != 0) {
node = node.getNext();
}
DoubleLinkedList newNode = new DoubleLinkedList(newCode, node, node.getNext());
node.getNext().setPrevious(newNode);
node.setNext(newNode);
this.setCode(this.getCode() + 1);
System.out.print("insertDouble-Success:");
this.write();
return true;
}
2.2.2、删
删除链表元素的方法就是将删除结点上一个结点的next指向删除结点的next,这里实现了删除位置与删除值两种方法。
由于链表插入时未实现去重,所以删除值时会删除链表中全部值为code的结点。
//无头单链表
public boolean deletePlace(int place) {
if (place == 0) {
System.out.println("deleteSingle-Fail:无法删除自己");
return false;
}
SingleLinkedList node = this;
while (node.next != null) {
if (place-- == 1) {
// 定位到删除元素的前一个
node.setNext(node.getNext().getNext());
System.out.print("deletePlaceSingle-Success:");
this.write();
return true;
} else {
node = node.getNext();
}
}
System.out.println("deletePlaceSingle-Fail:超出链表长度");
return false;
}
public boolean deleteCode(int code) {
SingleLinkedList node = this;
int deleteSum = 0;
while (node.next != null) {
if (node.next.getCode() == code) {
// 定位到删除元素的前一个
node.setNext(node.getNext().getNext());
deleteSum++;
}
node = node.getNext();
}
if (deleteSum == 0) {
System.out.println("deleteCodeSingle-Fail:超出链表长度");
return false;
} else {
System.out.print("deleteCodeSingle-Success:删除值为" + code + "的结点共" + deleteSum + "个,");
this.write();
return true;
}
}
对于有头双向循环链表同理。
//有头双向循环链表
public boolean deletePlace(int place) {
// 取余减少遍历次数
place = place % this.getCode();
if (place == 0) {
System.out.println("deleteDouble-Fail:头结点无法删除");
return false;
}
DoubleLinkedList node = this;
while (place-- > 1) {
node = node.getNext();
}
node.setNext(node.getNext().getNext());
node.getNext().setPrevious(node);
this.setCode(this.getCode() - 1);
System.out.print("deleteDouble-Success:");
this.write();
return true;
}
public boolean deleteCode(int code) {
// 取余减少遍历次数
DoubleLinkedList node = this.next;
int deleteSum = 0;
while (node != this) {
if (node.getCode() == code) {
node.getPrevious().setNext(node.getNext());
node.getNext().setPrevious(node.getPrevious());
this.setCode(this.getCode() - 1);
deleteSum++;
}
node = node.getNext();
}
if (deleteSum == 0) {
System.out.println("deleteCodeSingleDouble-Fail:超出链表长度");
return false;
} else {
System.out.print("deleteCodeDouble-Success:删除值为" + code + "的结点共" + deleteSum + "个,");
this.write();
return true;
}
}
2.2.3、查
查找的原理与删除同理,只是减少了对其余结点的操作,由于没有去重,返回为值为code的结点位置的数组。
//无头单链表
public ArrayList<Integer> seek(int code) {
SingleLinkedList node = this;
// 记录位置
int place = 0;
// 结果集
ArrayList<Integer> result = new ArrayList<>();
while (node != null) {
if (node.getCode() == code) {
result.add(0, place);
}
node = node.getNext();
place++;
}
if (result.isEmpty()) {
System.out.println("seekSingle-Fail:链表中无此节点");
return null;
} else {
System.out.println("seekSingle-Success");
return result;
}
}
//有头双向循环链表
public ArrayList<Integer> seek(int code) {
// 头结点存储链表长度,不在查找范围
DoubleLinkedList node = this.next;
// 记录位置
int place = 1;
// 存放结果集
ArrayList<Integer> result = new ArrayList<>();
while (node != this) {
if (node.getCode() == code) {
result.add(0, place);
}
node = node.next;
place++;
}
if (result.isEmpty()) {
System.out.println("seekDouble-Fail:链表中无此节点");
return null;
} else {
System.out.println("seekDouble-Success");
return result;
}
}
2.3、链表逆置
链表全部反转还是相对比较简单的,主要说明一下代码中的几个变量:
1、node是正在反转的结点,也是下一个结点的previous;
2、previous是node在原顺序的上一节点,也是反转后的node.next;
3、next存储的是node在原顺序中的下一节点,也是下一个需要反转的node。
在反转完最后一个链表结点时,node会指向null,而这时的previous存储的就是原顺序的尾结点,也是反转后的头结点。
// 全部反转无头单链表
public static SingleLinkedList turnWholeSingle(SingleLinkedList head) {
SingleLinkedList node = head;
//记录结点反转后的指向元素
SingleLinkedList previous = null;
//结点原来的下一元素
SingleLinkedList next = null;
while (node != null) {
next = node.next;
node.next = previous;
previous = node;
node = next;
}
System.out.print("turnSingle-success:");
previous.write();
return previous;
}
这里实现的局部反转是每group个元素反转一次,可以视为将原链表分割成head.length/group个子链表,各自反转后再进行头尾连接。
// 局部反转无头单链表
public static SingleLinkedList turnPartSingle(SingleLinkedList head, int group) {
// 递归结束条件
if (head == null || group <= 1) {
return head;
}
SingleLinkedList node = head;
SingleLinkedList previous = null;
SingleLinkedList next = null;
int g = group;
while (g-- > 0 && node != null) {
next = node.next;
node.next = previous;
previous = node;
node = next;
}
// 连接下一组反转的头结点
head.setNext(turnPartSingle(node, group));
return previous;
}
2.4、判断有环链表与查找环的入口
由于有头双向循环链表本身就是一个环,所以只判断无头单链表的环。
判断环时使用快慢指针,若存在环则快慢指针必定相遇。
查找环的入口需要一个
// 若链表存在环,返回环的入口;不存在则返回空
public SingleLinkedList isCircle() {
SingleLinkedList fast = this.next;
SingleLinkedList slow = this;
while (fast != null && fast.next != null) {
if (fast == slow) {
System.out.println("This SingleLinkedList has circle.");
// 查找环的入口
slow = this;
while (fast != slow && fast != null && slow != null) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
fast = fast.next.next;
slow = slow.next;
}
System.out.println("This SingleLinkedList has no circle.");
return null;
}
3、后记
链表操作不算复杂,也算是面试的一个常考点。由于代码纯手打可能在逻辑上存在不足,欢迎指出缺陷并讨论。
相关文章:
【算法】Java实现常用查找算法一(顺序查找、对分查找、插值查找、斐波那契查找)
【算法】Java实现常用查找算法二(树表查找、分块查找、哈希查找)
【算法】Java实现常用排序算法一(冒泡排序、选择排序、插入排序、堆排序、快速排序)
【算法】Java实现常用排序算法二(希尔排序、归并排序、计数排序、桶排序、基数排序)
【算法】Java线性表转化为二叉树与二叉树打印(优化思路过程)
【算法】二叉树的前序、中序、后序遍历与还原二叉树