目录
前言
单链表∶单向链表,默认只能从链表的头部遍历到链表的尾部实际的应用中很少见,太局限,只能从头遍历到尾部
引入
所以为了解决这个问题,就引入了双向链表这个概念
双向链表:对于该链表中的任意节点,即可通过该节点向后走,也可以通过该节点向前走。
双向链表实际工程中应用非常广泛,使用链表这个结构的首选。
JDK -> LinkedList ->双向链表
JDK里面 LinkedList 用的就是双向链表
双向链表的优点:
每个节点既保存了下一个节点的地址,也保存了上—个节点的地址
这样的话,就可以通过任意的节点从前向后或者从后向前
接口实现:
/**
* 增加
* addFirst(int val) 头插
* add(int index, int val) 在任意索引index处插入节点
* addLast(int val) 尾插
*
* 查找
* getByValue(int val) 查询第一个值为 val的索引为多少 不存在则返回 -1
* get(int index) 查询索引为index处的节点值为多少
* contains(int val) 查询是否包含指定值的节点
*
* 修改
* set(int index, int newVal) 修改索引为index位置的结点值为 newVal,返回修改前的节点值
*
* 删除
* remove(int index) 删除 index 处的节点,并返回删除的节点值
* removeFrist() 头删
* removeLast() 尾删
* removeValueOnce(int val) 删除第一个值为val的节点
* removeAllValue(int val) 删除全部值为 val 的节点
*/
开始实行
/** * 单链表的具体的每个节点 - 车厢类 */
准备工作:
/**
* 单链表的具体的每个节点 - 车厢类
*/
class DoubleNode {
// 前驱节点
DoubleNode prve;
// 当前节点值
int val;
// 后继节点
DoubleNode next;
// alt + insert 快捷键
//无参构造
public DoubleNode() {
}
//有参构造
public DoubleNode(int val) {
this.val = val;
}
//有参构造
public DoubleNode(DoubleNode prve, int val, DoubleNode next) {
this.prve = prve;
this.val = val;
this.next = next;
}
}
/** * 基于int的双向链表 - 火车 * 真正被用户使用的是火车类-双向链表对象 */
准备工作
public class Double_Linked_list {
// 有效节点的个数
private int size;
// 当前头节点
private DoubleNode head;
// 当前尾节点
private DoubleNode tail;
}
1. 实现头插法:分为链表中有无节点两种情况
// 在双向链表的头部插入新节点
public void addFirst(int val){
//直接用构造方法快速赋值
DoubleNode node = new DoubleNode(null,val,head);
if(head == null || tail == null){
//链表中没有节点时
head = tail = node;
}else{
//以前的头节点的前驱地址连接新的节点位置
head.prve = node;
head = node;//新节点成为新的头节点
}
size++;
}
2.补充toString方法打印链表
//补充toString方法打印链表
public String toString(){
String rep = "";
for (DoubleNode x = head; x != null ; x = x.next) {
rep += x.val;
rep += " -> ";
}
rep += "NULL";
return rep;
}
测试:
//双向链表的测试模块
public class Double_Test {
public static void main(String[] args) {
Double_Linked_list dll = new Double_Linked_list();
dll.addFirst(1);
dll.addFirst(2);
dll.addFirst(3);
dll.addFirst(4);
System.out.println(dll);
}
}
3. 精简下刚刚写的头插法。
你会发现,无论链表中是否存在节点,因为是头插都会有 head = node 这一步所以可以放在下面。而且也因为是头插,就执行判断此时的尾节点是否为空就行了。
// 在双向链表的头部插入新节点
public void addFirst(int val){
//直接用构造方法快速赋值
DoubleNode node = new DoubleNode(null,val,head);
// if(head == null || tail == null){
// //链表中没有节点时
// head = tail = node;
// }else{
// //以前的头节点的前驱地址连接新的节点位置
// head.prve = node;
// head = node;//新节点成为新的头节点
// }
//精简写法
if(tail == null){
// 链表中没有节点时
tail = node;
}else{
head.prve = node;//以前的头节点的前驱地址连接新的节点位置
}
head = node; // 对于头插来说,最终无论链表是否为空。head = node
size++;
}
4. 在双向链表的尾部插入新节点
现在为什么先写尾插而不是 index任意插?
因为之前的插入一个节点都是找前驱,头节点没有前驱所以先写了头插法。
而这次的双向,因为有了尾节点的存在所以可以找节点的后继开始插入节点,但尾节点是没有后继的,所以先写的尾插法,跟头插差不多。(当然按以前的写法也是可以的,但既然有了尾节点当然要利用起来,为了方便后面写 index任意插)
// 在双向链表的尾部插入新节点
public void addLast(int val){
DoubleNode node = new DoubleNode(tail, val, null);//直接用构造方法快速赋值
if(head == null){
head = node;
}else{
tail.next = node;
}
tail = node;
size ++;
}
测试:
public class Double_Test {
public static void main(String[] args) {
Double_Linked_list dll = new Double_Linked_list();
dll.addLast(1);
dll.addLast(2);
dll.addLast(3);
dll.addLast(4);
System.out.println(dll);
}
}
5. 在index索引处插入节点
之前我们都是找到前驱,此时找前驱是不可以灵活一点?
之前只能从head开始向后变量
假设咱链表有100个节点,我想在97号索引位置插入新元素,如果还是从头找那么我们弄这个双向链表的意义在哪从尾节点向前遍历就会快的多
我到底从前向后找还是从后向前找呢,那就设定一个条件
index < size / 2 =>从前向后,插入位置在前半部分
index > size / 2 =>从后向前,插入位置就在后半部分链表
发现:找前驱结点不光在插入会用到删除也会用到,所以为了方法,直接拉出来写成一个方法。
功能:根据索引值返回对于节点的地址。
注释:要用private封装,因为这个方法只是给我们实现功能提供的,并不是给别人使用的。私有。
//根据索引值返回对于节点的地址,不存在则返回 null
private DoubleNode node(int index){
DoubleNode x = null;
if(index < size / 2){
//从前往后走找需要 index 步
x = head;
for (int i = 0; i < index; i++) {
x = x.next;
}
}else{
//从后往前走找需要 size - 1 - index 步
x = tail;
for (int i = size - 1; i > index ; i--) {
//注意:这里不再是用next了而且前地址prve
x = x.prve;
}
}
return x;
}
现在可以开始写 在index索引处插入节点 add方法
/**
* 在 index 索引处插入一个节点
*/
public void add(int index, int val){
//判断索引的合法性
if(index < 0 || index > size){
System.out.println("插入的索引值错误");
return;
}
if(index == 0){
addFirst(val);
}else if(index == size){
addLast(val);
}else{
//到这里说明此时至少有三个节点
DoubleNode node = node(index);//索引位置处的节点。
DoubleNode cur = new DoubleNode(node.prve, val, node);//新节点
//在new这个对象的时候就已经用构造方法连接好两条线了。
node.prve.next = cur;//前驱连接新节点
node.prve = cur;//待插入位置的节点连接新节点
size ++;
}
}
测试:
public class Double_Test {
public static void main(String[] args) {
Double_Linked_list dll = new Double_Linked_list();
dll.addLast(1);
dll.addLast(2);
dll.addLast(3);
dll.addLast(4);
System.out.println(dll);
dll.add(2,6);
System.out.println(dll);
}
}
6. 查询
6.1 查询第一个值为 val的索引为多少
6.2 查询索引为index处的节点值为多少
6.3 查询是否包含指定值的节点
注释:为了写程序方便,把判断索引合理性写成一个方法 rangeCheck
/**
* 查询第一个值为 val的索引为多少
* 不存在则返回 -1
*/
public int getByValue(int val){
DoubleNode rep = head;
for (int i = 0; i < size; i++) {
if(rep.val == val){
return i;
}
rep = rep.next;
}
return -1; //循环里面没找到就代表不存在
}
/**
* 查询索引为index处的节点值为多少
*/
public int get(int index){
if(rangeCheck(index)){
return node(index).val;
}
return -1;
}
/**
* 查询是否包含指定值的节点
*/
public boolean contains(int val){
return getByValue(val) != -1;
}
/**
* 判断给的index索引是否合理
*/
private boolean rangeCheck(int index){
if(index < 0 || index >= size){
return false;
}
return true;
}
测试:
//双向链表的测试模块
public class Double_Test {
public static void main(String[] args) {
Double_Linked_list dll = new Double_Linked_list();
dll.addLast(1);
dll.addLast(2);
dll.addLast(3);
dll.addLast(4);
System.out.println(dll);
System.out.println("查询第一个值为 val的索引为多少:");
System.out.println(dll.getByValue(4));
System.out.println(dll.getByValue(1));
System.out.println(dll.getByValue(5));
System.out.println("----------------");
System.out.println("查询索引为index处的节点值为多少:");
System.out.println(dll.get(0));
System.out.println(dll.get(3));
System.out.println(dll.get(4));
System.out.println("----------------");
System.out.println("查询是否包含指定值的节点:");
System.out.println(dll.contains(1));
System.out.println(dll.contains(4));
System.out.println(dll.contains(0));
System.out.println(dll.contains(5));
}
}
7. 修改索引为index位置的结点值为 newVal,返回修改前的节点值
public int set(int index, int newVal){
DoubleNode cur = node(index);
int rep = cur.val;
cur.val = newVal;
return rep;
}
测试:
//双向链表的测试模块
public class Double_Test {
public static void main(String[] args) {
Double_Linked_list dll = new Double_Linked_list();
dll.addLast(1);
dll.addLast(2);
dll.addLast(3);
dll.addLast(4);
System.out.println(dll);
System.out.println(dll.set(0,6));
System.out.println(dll);
System.out.println(dll.set(4,9));
System.out.println(dll);
}
}
8. 删除当前双向链表中的node节点
注释:这个方法是私有private封装的,是方便我们等会写删除功能。
如何删除两边中的一个节点?
这里提出一个很重要的思修: 分治思想
概念:
1. 先处理前驱节点的事儿,完全不管后继
2. 等前驱部分全部处理完毕再单独处理后继情况
删除一个节点一共也就四种情况:
1.前空后空 2.前不空后空 3.前不空后不空 4.前空后不空注释:删除头节点是 前空后不空, 删除尾节点是 后空前不空
//删除当前双向链表中的node节点
private void unlink(DoubleNode node){
// 1.前空后空
// 2.前不空后空
// 3.前不空后不空
// 4.前空后不空
DoubleNode prve = node.prve;//前驱
DoubleNode next = node.next;//后继
//现在删除就有三种情况,头节点,尾节点.注释:这里的前部分和后部分都是以node为中心的
//先处理前半部分
if(prve == null){
//删除的是头节点,那就更新头节点位置
head = next;//头节点变为后继节点
//注释:这里不要着急断开原来头节点的next连接,那算后半部分
//现在这里只处理前半部分
}else{
// 前驱不为空的情况
prve.next = next;
node.prve = null;//断开连接
}
//现在处理后半部分
if(next == null){
//删除的是尾节点,那就更新尾节点位置
tail = prve;
}else{
// 后继不为空的情况
next.prve = prve;
node.next = null;//断开连接
}
size --;
}
注释:
这里说的前半部分和后半部分都是以 node待删除节点为参照物的,node的前部分连线问题和
node后半部分连线问题。
9. 删除 index 处的节点,并返回删除的节点值
和 头删除,尾删。直接利用刚刚写的 unlike 方法
//删除 index 处的节点,并返回删除的节点值
public int remove(int index){
if(rangeCheck(index)){
DoubleNode node = node(index);//返回索引处的节点地址
int rep = node.val;
unlink(node);
return rep;
}
System.out.println("输入删除的索引错误");
return -1;
}
//头删
public int removeFrist(){
return remove(0);
}
//尾删
public int removeLast(){ return remove(size - 1); }
10. 最后两个删除val模块
删除出现第一个值为val的结点
删除全部值为 val 的节点
//删除出现第一个值为val的结点
public void removeValueOnce(int val){
for (DoubleNode x = head; x != null ; x = x.next) {
if(x.val == val){
unlink(x);
break;
}
}
}
//删除全部值为 val 的节点
public void removeAllValue(int val) {
for (DoubleNode x = head; x != null; ) {
if (x.val == val) {
//如果当前的节点要被删除,保存它下一个节点地址
DoubleNode successor = x.next;
unlink(x);
x = successor;//让循环的x连接上后面的链表
} else {
x = x.next;
}
}
}
//注释:我们unlink删除节点时吧它连接的全断开了
//要删除全部的val时会发生什么? 当时的在循环中的链表
//直接失去的后继节点的地址,所以我们要在unlink的时候保存下一个节点地址
注释:这里唯一要注意的就是在我们删除全部val值时,我们的unlink会把当前的node节点连接的线全部断开,导致正在循环的 x 直接就结束了,要解决这个问题就要在unlink删除这个节点前先保存下一个节点的地址。 然后删除后连接后面的链表
测试:
public class Double_Test {
public static void main(String[] args) {
Double_Linked_list dll = new Double_Linked_list();
dll.addLast(3);
dll.addLast(1);
dll.addLast(3);
dll.addLast(1);
dll.addLast(2);
dll.addLast(2);
dll.addLast(2);
dll.addLast(2);
dll.addLast(2);
System.out.println(dll);
dll.removeValueOnce(1);
System.out.println(dll);
dll.removeAllValue(2);
System.out.println(dll);
}
}
总代码参考
package seqlist.双向链表;
/**
* 基于int的双向链表 - 火车
* 真正被用户使用的是火车类-双向链表对象
*/
public class Double_Linked_list {
// 有效节点的个数
private int size;
// 当前头节点
private DoubleNode head;
// 当前尾节点
private DoubleNode tail;
// 在双向链表的头部插入新节点
public void addFirst(int val){
//直接用构造方法快速赋值
DoubleNode node = new DoubleNode(null,val,head);
// if(head == null || tail == null){
// //链表中没有节点时
// head = tail = node;
// }else{
// //以前的头节点的前驱地址连接新的节点位置
// head.prve = node;
// head = node;//新节点成为新的头节点
// }
//精简写法
if(tail == null){
// 链表中没有节点时
tail = node;
}else{
head.prve = node;//以前的头节点的前驱地址连接新的节点位置
}
head = node; // 对于头插来说,最终无论链表是否为空。head = node
size++;
}
// 在双向链表的尾部插入新节点
public void addLast(int val){
DoubleNode node = new DoubleNode(tail, val, null);//直接用构造方法快速赋值
if(head == null){
head = node;
}else{
tail.next = node;
}
tail = node;
size ++;
}
/**
* 在 index 索引处插入一个节点
*/
public void add(int index, int val){
//判断索引的合法性
if(index < 0 || index > size){
System.out.println("插入的索引值错误");
return;
}
if(index == 0){
addFirst(val);
}else if(index == size){
addLast(val);
}else{
//到这里说明此时至少有三个节点
DoubleNode node = node(index);//索引位置处的节点。
DoubleNode cur = new DoubleNode(node.prve, val, node);//新节点
//在new这个对象的时候就已经用构造方法连接好两条线了。
node.prve.next = cur;//前驱连接新节点
node.prve = cur;//待插入位置的节点连接新节点
size ++;
}
}
//根据索引值返回对于节点的地址,不存在则返回 null
private DoubleNode node(int index){
DoubleNode x = null;
if(index < size / 2){
//从前往后走找需要 index 步
x = head;
for (int i = 0; i < index; i++) {
x = x.next;
}
}else{
//从后往前走找需要 size - 1 - index 步
x = tail;
for (int i = size - 1; i > index ; i--) {
//注意:这里不再是用next了而且前地址prve
x = x.prve;
}
}
return x;
}
/**
* 查询第一个值为 val的索引为多少
* 不存在则返回 -1
*/
public int getByValue(int val){
DoubleNode rep = head;
for (int i = 0; i < size; i++) {
if(rep.val == val){
return i;
}
rep = rep.next;
}
return -1; //循环里面没找到就代表不存在
}
/**
* 查询索引为index处的节点值为多少
*/
public int get(int index){
if(rangeCheck(index)){
return node(index).val;
}
return -1;
}
/**
* 查询是否包含指定值的节点
*/
public boolean contains(int val){
return getByValue(val) != -1;
}
/**
* 判断给的index索引是否合理
*/
private boolean rangeCheck(int index){
if(index < 0 || index >= size){
return false;
}
return true;
}
/**
* 修改索引为index位置的结点值为 newVal,返回修改前的节点值
*/
public int set(int index, int newVal){
DoubleNode cur = node(index);
int rep = cur.val;
cur.val = newVal;
return rep;
}
//删除当前双向链表中的node节点
private void unlink(DoubleNode node){
// 1.前空后空
// 2.前不空后空
// 3.前不空后不空
// 4.前空后不空
DoubleNode prve = node.prve;//前驱
DoubleNode next = node.next;//后继
//现在删除就有三种情况,头节点,尾节点.注释:这里的前部分和后部分都是以node为中心的
//先处理前半部分
if(prve == null){
//删除的是头节点,那就更新头节点位置
head = next;//头节点变为后继节点
//注释:这里不要着急断开原来头节点的next连接,那算后半部分
//现在这里只处理前半部分
}else{
// 前驱不为空的情况
prve.next = next;
node.prve = null;//断开连接
}
//现在处理后半部分
if(next == null){
//删除的是尾节点,那就更新尾节点位置
tail = prve;
}else{
// 后继不为空的情况
next.prve = prve;
node.next = null;//断开连接
}
size --;
}
//删除 index 处的节点,并返回删除的节点值
public int remove(int index){
if(rangeCheck(index)){
DoubleNode node = node(index);//返回索引处的节点地址
int rep = node.val;
unlink(node);
return rep;
}
System.out.println("输入删除的索引错误");
return -1;
}
//头删
public int removeFrist(){
return remove(0);
}
//尾删
public int removeLast(){ return remove(size - 1); }
//删除出现第一个值为val的结点
public void removeValueOnce(int val){
for (DoubleNode x = head; x != null ; x = x.next) {
if(x.val == val){
unlink(x);
break;
}
}
}
//删除全部值为 val 的节点
public void removeAllValue(int val) {
for (DoubleNode x = head; x != null; ) {
if (x.val == val) {
// x就是待删除的结点
//当前的节点要被删除,保存它下一个节点地址
DoubleNode successor = x.next;
unlink(x);
x = successor;//让循环的x连接上后面的链表
} else {
x = x.next;
}
}
}
//注释:我们unlink删除节点时吧它连接的全断开了
//要删除全部的val时会发生什么? 当时的在循环中的链表
//直接失去的后继节点的地址,所以我们要在unlink的时候保存下一个节点地址
//补充toString方法打印链表
public String toString(){
String rep = "";
for (DoubleNode x = head; x != null ; x = x.next) {
rep += x.val;
rep += " -> ";
}
rep += "NULL";
return rep;
}
}
/**
* 单链表的具体的每个节点 - 车厢类
*/
class DoubleNode {
// 前驱节点
DoubleNode prve;
// 当前节点值
int val;
// 后继节点
DoubleNode next;
// alt + insert 快捷键
//无参构造
public DoubleNode() {
}
//有参构造
public DoubleNode(int val) {
this.val = val;
}
//有参构造
public DoubleNode(DoubleNode prve, int val, DoubleNode next) {
this.prve = prve;
this.val = val;
this.next = next;
}
}