一、什么是双向链表
相对于单向链表只有下一个节点域而言,双向链表有上一个节点域和下一个节点域。
头节点只有下一个节点域,没有上一个节点域和数据,尾节点没有下一个节点域,只有数据和上一个节点域。
因为是双向的,所以可以双向链接。
二、java代码实现
package mypackage;
/**
* 双向链表的创建
* 要实现的功能包括:
* 初始化
* 返回长度,即节点个数
* 插入、删除节点
* 根据索引获取节点
* 根据节点获取位置
* 判断是否为空
* 返回第一个元素和最后一个元素
*
* 双向链表的核心在于节点类,有了节点类,才能更好的表示节点之间的链接关系
*
* 注意,关于第i个节点的约定,第1个节点是头节点后的第一个节点,依次类推.
* 我们把第一个节点就称之为第一个节点,注意没有索引的概念
* 即使有索引的话,也是从1开始的,而不是从0开始的
*/
import java.util.Iterator;
//<T>泛型表示支持任意类型
//implements Iterable<T>可实现遍历
class WowWayLinkList<T> implements Iterable<T>{
//记录头节点
private Node head;
//记录尾节点
private Node last;
//记录链表的长度,即节点的个数
private int N;
//定义节点类,这一步至关重要
//注意构造方法的参数有一个是pre,一个next,这个类型是Node,正是我们定义的这个类Node
//这样的话,我们每个节点就可以传入一个参数,pre和next,然后用当前结点调用.pre和.next属性就可以获得上一个、下一个节点
//这就是双向链表的核心
private class Node{
//数据项和下一个节点项
T data;
Node pre;
Node next;
//构造方法
public Node(T data, Node pre,Node next){
this.data=data;
this.pre=pre;
this.next=next;
}
}
//构造方法
public WowWayLinkList(){
//初始化头节点,初步的头节点的数据项、上一个节点和下一个节点项都是null
//头节点的数据项本身就定义为null,他的作用只是指向链表的第一个由数据的节点而已
//尾节点的下一个节点项本身就定义为null,表示链表的结束
this.head=new Node(null,null,null);
// 开始时没有尾节点
this.last=null;
// 节点个数为0
this.N=0;
}
//清空链表,使得头节点不指向任何节点,尾节点为null,且节点个数为0
public void clear(){
head.next=null;
last=null;
this.N=0;
}
//获取节点个数,不包括头节点,但是包括尾节点
public int length(){
return N;
}
//判断链表是否为空
public boolean isEmpty(){
return N==0;
}
//获取第一个元素,如果为空,返回null,如果不为空返回头节点后的第一个元素
public T getfirst(){
if(isEmpty()){
return null;
}
else {
return head.next.data;
}
}
//获取最后一个元素,如果为空,返回null,如果不为空返回尾节点的元素
public T getlast(){
if(isEmpty()){
return null;
}
else {
return last.data;
}
}
//直接在尾部添加节点,先判断是否为空
//如果为空,创建新节点,让头节点指向新节点,且将新节点变为尾节点
//如果不为空,创建新节点,让尾节点指向新节点,且将新节点变为尾节点
public void append(T data) {
if (isEmpty()) {
Node newnode = new Node(data, head, null);
head.next = newnode;
last = newnode;
N++;
} else {
//原尾节点
Node oldlast=last;
Node newnode = new Node(data,oldlast, null);
oldlast.next = newnode;
// 新的尾节点即为添加在尾部的新节点
last = newnode;
N++;
}
}
//向指定位置插入节点,使得这个位置之前的节点指向这个新节点,
//这个新节点指向这个位置的原本的那个节点
//同时特别注意,因为是双向链表,要双向指向
//其实就是先打断,再链接,链接要双向
//i第几个节点,和索引没有任何关系,链表没有索引的概念
public void insert(int i,T data){
//如果i==N就代表是要在最后添加,直接调用append即可
if(i==N){
append(data);
}
//如果在非最后添加
else{
// 从头节点开始循环,循环一次,到第一个节点,循环第i-1次,循环到第i-1个节点
// 这就找到了第i-1个节点
Node prenode=head;
//找到待插入节点的前一个节点
for (int index=1;index<=i-1;index++){
prenode=prenode.next;
}
// 第i个节点
Node curnode=prenode.next;
// 创建新节点,并且新节点指向第i-1个节点和第i个节点,注意是双向指向
Node newnode=new Node(data,prenode,curnode);
//将前一个节点指向新节点
prenode.next=newnode;
// 将第i个节点指向新节点
curnode.pre=newnode;
N++;
}
}
//返回第i个节点的值
public T get(int i){
//从头节点向后循环找,循环1此找打第一个节点,循环i次找到第i个节点
Node n=head;
for (int index=1;index<=i;index++){
n=n.next;
}
//找到节点后,返回节点的数据
return n.data;
}
//删除指定位置节点,并返回删除的值
//如果删除的是最后一个节点,将倒数第二个节点指向null,且变为last节点即可
//如果删除的是非最后一个节点,将i-1个节点指向i+1个节点,且将i+1个节点指向i-1个节点即可
//注意双向指向
public T remove(int i){
//如果删除的是最后一个节点,将倒数第二个节点指向null,且变为last节点即可
if(i==N){
Node prenode=head;
for (int index=1;index<=i-1;index++){
prenode=prenode.next;
}
// 最后一个节点
Node curnode=prenode.next;
// 使最后一个节点的前一个节点指向null
prenode.next=null;
last=prenode;
N--;
// 返回最后一个节点的元素值
return curnode.data;
}
//如果删除的是非最后一个节点,将i-1个节点指向i+1个节点,且将i+1个节点指向i-1个节点即可
else{
// 循环找到前一个节点i-1
Node prenode=head;
for (int index=1;index<=i-1;index++){
prenode=prenode.next;
}
// 指定节点i
Node curnode=prenode.next;
// 指定节点的后一个节点i+1
Node nextnode=curnode.next;
// 使得指定节点的前一个节点指向指定节点的后一个节点,即i-1和i+1节点双向指向
prenode.next=nextnode;
nextnode.pre=prenode;
N--;
// 返回指定节点的元素i
return curnode.data;
}
}
//查找某个节点第一次出现的位置,即是第几个元素,从1开始
//循环查找,找到就返回,没找到就返回-1
public int indexOf(T data){
Node n=head;
for (int index=1;index<=N;index++){
n=n.next;
if(n.data.equals(data)){
return index;
}
break;
}
return -1;
}
//实现遍历
// 因为是双向链表,按理说可以从前往后遍历也可以从后往前遍历,这里选择从前往后遍历
//遍历的话,要重写此方法
@Override
public Iterator<T> iterator() {
// 返回的Iterator对象,创建一个内部类实现这个接口
return new TIterator();
}
// 创建一个内部类实现Iterator接口
public class TIterator implements Iterator {
// 定义一个遍历的节点
private Node n;
public TIterator(){
// 初始化为头节点
this.n=head;
}
//重写两个方法
@Override
public boolean hasNext() {
// 如果为尾节点,超出会停止遍历
return n.next!=null;
}
@Override
public Object next() {
// 这个方法会遍历得每个节点
n=n.next;
return n.data;
}
}
}
//测试
public class MyJava {
public static void main(String[] args) {
// 创建一个链表
WowWayLinkList<Object> list = new WowWayLinkList<>();
// 添加节点节点
list.append(1);
list.append(2);
list.append(3);
//开始遍历
for(Object ls:list){
System.out.println("遍历元素---"+ls);
}
System.out.println("第一个元素"+list.getfirst());
System.out.println("最后一个元素"+list.getlast());
// 返回节点个数
int length1 = list.length();
System.out.println("元素个数"+length1);
// 插入节点
list.insert(2, 100);
//开始遍历
for(Object ls:list){
System.out.println("插入节点后遍历元素---"+ls);
}
// 删除节点
Object remove = list.remove(1);
System.out.println("删除指定位置处的节点元素是"+remove);
//开始遍历
for(Object ls:list){
System.out.println("删除节点后遍历节点元素---"+ls);
}
// 返回第几个节点元素
System.out.println("第1个节点的元素是"+list.get(1));
// 返回某个值的位置
System.out.println("指定节点元素对应的位置为"+list.indexOf(100));
// 清空
list.clear();
// 是否为空
System.out.println("判断是否为空"+list.isEmpty());
}
}
结果:
三、双向链表代码可改进的点
上面的代码,其实和单向链表类似,那么双向链表的双向有何作用呢?
既然是双向,那么就要充分利用可以双向查找的特点,比如我们的插入节点的代码:
public void insert(int i,T data){
//如果i==N就代表是要在最后添加,直接调用append即可
if(i==N){
append(data);
}
//如果在非最后添加
else{
// 从头节点开始循环,循环一次,到第一个节点,循环第i-1次,循环到第i-1个节点
// 这就找到了第i-1个节点
Node prenode=head;
//找到待插入节点的前一个节点
for (int index=1;index<=i-1;index++){
prenode=prenode.next;
}
// 第i个节点
Node curnode=prenode.next;
// 创建新节点,并且新节点指向第i-1个节点和第i个节点,注意是双向指向
Node newnode=new Node(data,prenode,curnode);
//将前一个节点指向新节点
prenode.next=newnode;
// 将第i个节点指向新节点
curnode.pre=newnode;
N++;
}
}
其实这个代码我们始终是从头节点到尾节点遍历去找到待插入的那个位置,假如节点很多的时候,如果我们要插入的位置是倒数第3个位置,这个时候从头节点开始查找就会很慢,因此我们是可以判断待插入位置来决定是从头节点还是尾节点开始遍历,如果i处于前半部分,可以从头节点开始找,如果是处于后半部分,则可以选择从尾节点开始查找,这样就会更快,改进一下插入的代码如下:
public void insert(int i,T data) {
//如果i==N就代表是要在最后添加,直接调用append即可
if (i == N) {
append(data);
} else if (i <= N / 2) {
// 从头节点开始循环,循环一次,到第一个节点,循环第i-1次,循环到第i-1个节点
// 这就找到了第i-1个节点
Node prenode = head;
//找到待插入节点的前一个节点
for (int index = 1; index <= i - 1; index++) {
prenode = prenode.next;
}
// 第i个节点
Node curnode = prenode.next;
// 创建新节点,并且新节点指向第i-1个节点和第i个节点,注意是双向指向
Node newnode = new Node(data, prenode, curnode);
//将前一个节点指向新节点
prenode.next = newnode;
// 将第i个节点指向新节点
curnode.pre = newnode;
N++;
}
// 这里的情况是i>N/2
else {
// 从尾节点开始循环,循环一次,到倒数第二个节点,循环第N-i次,循环到第i个节点
// 这就找到了第i个节点
Node curnode = last;
//找到待插入节点
for (int index = 1; index <= N - i; index++) {
curnode = curnode.pre;
}
// 第i-1个节点
Node prenode = curnode.pre;
// 创建新节点,并且新节点指向第i-1个节点和第i个节点,注意是双向指向
Node newnode = new Node(data, prenode, curnode);
//将前一个节点指向新节点
prenode.next = newnode;
// 将第i个节点指向新节点
curnode.pre = newnode;
N++;
}
}
同样的道理,在查找和删除的时候,也可以根据位置来选择从头节点还是尾节点遍历,就不多加描述了
四、关于循环几次的思考
假设现在有一组数据编号分别为1,2,3,4,5,6,7,8,9,10
那么从1到6处需要走几步?当然是6-1=5步,也就是要循环5次;
从3到6要走几步?当然是6-3=3步,也就是要循环3次;
再来看从10到4要走几步?当然是10-4=6步,也就是要循环6次;
更普遍的从i走到j需要几步?当然是(j-i)的绝对值步,也就是要循环(j-i)的绝对值次;
好了循环次数决定了,那么循环应该怎么写呢?特别是基准值应该是怎样的?
for(index=1;index<=(j-i)的绝对值;index++)即可,从1数到(j-i)的绝对值次不就是循环多少次吗?所以说从1到(j-i)的绝对值次就是循环多少次。