从1开始学Java数据结构与算法——单链表与双链表

  • 单链表的特点
  • 单链表的代码思路分析
  • 方法详细思路分析
  • 带头节点单链表的代码实现
  • 小问题:
  • 双链表的方法思路分析
  • 带头节点双链表的代码实现


单链表的特点

1.链表是以节点的方式存储的

2.每个节点包含data域,用来存放数据;和next域,用来指向下一个节点

3.链表的各个节点不一定是连续存储

4.链表分为带头节点的和不带头节点的

5.单链表在内存中的布局如下图

java 单项链表 排序 java单链表和双链表的区别_算法

单链表的代码思路分析

1.先创建一个节点类,用于表示节点,里面包含结点的数据域所需的各个变量以及next域
2.创建一个头节点,数据data域和next初始化都为空
3.直接添加链表尾部的增加方法
4.按顺序添加在链表中的方法
5.删除节点的方法
6.修改节点数据域的方法
7.查询方法

方法详细思路分析

方法

思路

直接将节点添加在链表尾部

1.给个临时变量用于遍历,初始化指向头节点;

2.开始遍历链表到最后一个节点;

3.让最后一个节点的next域指向新插入的节点即可

方法

思路

按顺序添加在链表中

1.给个临时变量用于遍历,初始化指向头节点;

2.开始遍历链表找到需要插入的位置的前一个节点A;

3.让新插入节点的next域指向A节点的next域

4.再让A节点的next域指向新插入的节点

(这两句话可能有点懵,如下图解)

java 单项链表 排序 java单链表和双链表的区别_java_02


执行3时候,生成②蓝色的线

执行4的手,接触③黑色的线,同时生成①红色的线

java 单项链表 排序 java单链表和双链表的区别_java_03

方法

思路

删除节点

1.给个临时变量用于遍历,初始化指向头节点;

2.开始遍历链表到要删除节点的前一个结点A;

3.让A结点的next域指向它的next域结点的next节点(A.next = A.next.next)

修改节点数据域

1.给个临时变量用于遍历,初始化指向头节点的下一个节点,即指向链表的第一个节点;

2.开始遍历链表到要修改的节点A;

3.对A的数据域要修改的部分进行赋值即可

查询方法

1.给个临时变量用于遍历,初始化指向头节点的下一个节点,即指向链表的第一个节点;

2.开始遍历链表到最后一个节点逐个进行输出即可;

带头节点单链表的代码实现

首先肯定需要一个表示节点信息的类,其中包含节点的data数据域和next域

/**
 * 节点类
 * @author centuowang
 * @param
 * 		no:数据域中人物的编号
 * 		no:数据域中人物的名称
 * 		nickname:数据域中人物的小名
 *		next:next域,用于指向下一个节点
 */
class PersonNode{
	
	public int no;
	public String name;
	public String nickname;
	public PersonNode next;
	
	//构造器,注意构造器中不需要传入next域
	public PersonNode(int no, String name, String nickname) {
		this.no = no;
		this.name = name;
		this.nickname = nickname;
	}

	//重写一些toString方法,方便显示
	@Override
	public String toString() {
		return "PersonNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
	}
	
}

接着就可以开始编写我们的链表类了,在里面封装增删改查的相关方法

/**
 * 链表的关键部分,封装了增删改查的方法类
 * @author centuowang
 * @method 
 * 		add(PersonNode node):直接将新插入的节点添加在链表尾部
 * 		orderadd(PersonNode node):对插入的节点依据编号进行排序
 * 		delete(int no):依据编号删除节点
 * 		update(PersonNode node):修改节点
 * 		show():遍历显示输入链表
 */
class LinkList{
	
	//先创建一个头节点
	private PersonNode head = new PersonNode(0,"","");
	
	//不按顺序增加方法
	//思路:遍历找到链表中的最后一个节点,然后讲其的next指向新加入的节点即可
	public void add(PersonNode node) {
		//因为头节点不能动,所以需要一个临时变量temp
		PersonNode temp = head;
		//遍历链表,找到链表的最后一个节点
		while(true) {
			if(temp.next == null){
				//如果该临时节点变量的next域为空,说明它已经是最后一个节点了
				break;//将死循环停止
			}
			//否则,就temp后移,继续判断是不是最后一个节点
			temp = temp.next;
		}
		//跳出循环之后,开始加入,将最后一个节点的next域指向新传入的节点
		temp.next = node;
	}
	
	//按找节点中数据域的no编号排序加入数据
	public void orderadd(PersonNode node) {
		//同样的需要一个临时变量,初始化指向头节点
		PersonNode temp = head;
		//还需要一个标志变量,去标志是否已经存在该编号的节点
		boolean flag = false;
		//也是一样的需要遍历
		while(true){
			if(temp.next == null) {
				//此时说明已经遍历到了最后一个节点
				break;
			}else if(temp.next.no > node.no) {//这里注意了,遍历到什么时候停止呢?遍历到需要插入位置的前一个节点停止
				break;
			}else if(temp.next.no == node.no) {
				//此时说明,原链表数据里,已经存在该编号的节点,标志位置为true
				flag = true;
				break;
			}
			//temp后移
			temp = temp.next;
		}
		if(flag) {
			System.out.printf("编号为%d的节点已存在,不能重复加入\n",node.no);
		}else {
			//否则的话,开始执行插入
			node.next = temp.next;
			temp.next = node;
		}
	}
	
	//删除方法,根据节点编号进行删除
	//思路:先遍历到要删除节点的前一个节点,然后改变该节点的next域,使其直接指向要删除节点的后一个节点即可
	public void delete(int no) {
		
		//同样的需要一个临时变量,初始化指向头节点
		PersonNode temp = head;
		//选哟一个标志位去标志是否找到了要删除的节点的前一个节点位置
		boolean flag = false;
		//开始遍历
		while(true) {
			if(temp.next == null) {
				//已经遍历到链表最后,说明没有要删除的节点,直接终止循环
				break;
			}
			if(temp.next.no == no) {
				//如果找到了要删除节点的前一个节点,更改标志位,终止循环
				flag = true;
				break;
			}
			//temp后移
			temp = temp.next;
		}
		if(flag) {
			temp.next = temp.next.next;
		}else {
			System.out.printf("要删除的第%d节点不存在\n",no);
		}
	}
	
	//修改方法(根据节点编号来修改)
	//思路:先遍历到需要修改的节点编号
	public void update(PersonNode node) {
		
		if(head.next == null) {
			System.out.println("链表为空");
		}
		//同样的需要一个临时变量
		PersonNode temp = head.next;
		//需要一个标志位来判断是否找到
		boolean flag = false;
		while(true) {
			if(temp == null) {
				break;
			}
			if(temp.no == node.no) {
				//找到了要删除的节点,标志位置为true,终止循环
				flag = true;
				break;
			}
			//temp后移
			temp = temp.next;
		}
		if(flag) {
			//如果找到,进行修改
			temp.name = node.name;
			temp.nickname = node.nickname;
		}else {
			//否则给出提示
			System.out.printf("需要修改的第%d个节点不存在\n",node.no);
		}
	}
	
	//查询方法
	public void show() {
		if(head.next == null) {
			System.out.println("链表为空");
		}else {
			//同样的需要一个临时变量,这里我们不打印头节点,所以temp直接指向head.next
			PersonNode temp = head.next;
			while(true) {
				if(temp == null) {
					//如果该临时节点变量为空,说明已经遍历完,直接break,终止循环
					break;
				}
				//如果还没遍历完,就将节点内容打印出来
				System.out.println(temp);
				//然后后移一个继续判断
				temp = temp.next;
			}
		}
	}
	
	
}

小问题:

1.在链表的修改方法中,有人提出,为什么不直接将要修改的节点删除,再用新数据的节点插入到该位置上。

这样的话,就需要对一个功能调用两种方法,代码非常冗余,而且时间复杂度上,遍历删除一个O(n),在指定位置插入一个O(n),明显没有直接写个方法就一个O(n)来的快把

2.我们可以发现,在单链表中的删除功能中不能对自己进行删除,只能定位到要删除节点的前一个节点对其操作。而采用双向链表可以对自身进行删除

单链表只能找后继,如果我们需要找一个节点的前驱,就需要用双向链表可以

java 单项链表 排序 java单链表和双链表的区别_java_04

双链表的方法思路分析

首先在节点类中,需要加多一个指向前驱的变量pre,下面来分析具体的方法思路

方法

代码思路

在链表尾部添加节点

遍历到最后一个节点A

A.next = 新加入的节点

新加入的节点.pre = A

修改节点

和单链表一样

查询显示节点

和单链表一样

上面三个方法都没什么好说的,主要分析下面两个方法:

方法

代码思路

按节点编号添加节点

1.找到要加入的位置的前一个节点A

2.新加入节点的next指向A.next
3.A.next指向新加入的节点
4.新加入节点的pre指向A
5.新加入节点的.next.pre指向新加入的节点

如下图解

java 单项链表 排序 java单链表和双链表的区别_java 单项链表 排序_05


这里我们需要将2号节点加入到1和3之间,那么这里的A节点就是1号节点

执行2,产生上图中①号线

执行3,产生②号线,同时③号线解除

执行4,产生④号线

执行5,产生⑤号线,同时⑥号线解除

java 单项链表 排序 java单链表和双链表的区别_数据结构_06

方法

代码思路

删除节点

1.找到要删除的节点A

2.A.pre.next = A.next
3.A.next.pre = A.pre

如下图解

java 单项链表 排序 java单链表和双链表的区别_算法_07


我们删除2号节点,那么这里的A也就是 2号节点本身

执行2,产生①号线,同时③号线解除

执行3,产生②号线,同时四号线解除

java 单项链表 排序 java单链表和双链表的区别_java_08


这里有人会说了,那要删除的节点还有两个线没解除啊,如上图紫色部分。

这两个是这个节点自己本身的next域和pre域,在删除的时候不必理睬,他们会随着这个节点一起被Java的回收机制回收掉

带头节点双链表的代码实现

同样的先创建一个节点类

/**
 * 节点类
 * @author centuowang
 * @param
 * 		no:节点数据域的编号
 * 		name、nickname:节点数据域的可变数据部分
 * 		next:节点的next域,指向后继节点
 * 		pre:节点的pre域,指向前驱节点
 *
 */
class DoublePersonNode{
	
	public int no;
	public String name;
	public String nickname;
	public DoublePersonNode next;
	public DoublePersonNode pre;
	
	//构造器
	public DoublePersonNode(int no, String name, String nickname) {
		this.no = no;
		this.name = name;
		this.nickname = nickname;
	}

	@Override
	public String toString() {
		return "DoublePersonNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
	}
	
}

接下来开始链表类的编写

/**
 * 链表的关键部分,封装了增删改查的方法类
 * @author centuowang
 * @method 
 * 		add(DoublePersonNode node):直接将新插入的节点添加在链表尾部
 * 		orderadd(DoublePersonNode node):对插入的节点依据编号进行排序
 * 		delete(int no):依据编号删除节点
 * 		update(DoublePersonNode node):修改节点
 * 		show():遍历显示输入链表
 *
 */
class DoubleLink{
	
	//先创建一个头节点
	DoublePersonNode head = new DoublePersonNode(0,"","");
	
	//直接增加在链表尾部
	public void add(DoublePersonNode node) {
		//需要一个临时变量
		DoublePersonNode temp = head;
		//遍历到链表的尾部
		while(true) {
			if(temp.next == null) {
				//temp已经指到链表尾部
				break;
			}
			//后移
			temp = temp.next;
		}
		//开始加入节点
		temp.next = node;
		node.pre = temp;
	}
	
	//自动按节点顺序增加
	public void orderadd(DoublePersonNode node) {
		//需要一个临时变量
		DoublePersonNode temp = head;
		//给一个标志位,标志是否能插入
		boolean flag = true;
		//遍历链表,找到需要插入的位置前的一个节点
		while(true) {
			if(temp.next == null) {
				//遍历到了链表的尾部
				break;
			}if(temp.next.no > node.no) {
				//找到位置
				break;
			}if(temp.next.no == node.no) {
				//该编号的节点已存在
				flag = false;
				break;
			}
			//后移
			temp = temp.next;
		}
		if(flag) {
			//能插入
			node.next = temp.next;
			temp.next = node;
			node.pre = temp;
			//如果不为最后一节点
			if(node.next != null) {
				node.next.pre = node;
			}
			System.out.printf("%d编号节点成功插入\n",node.no);
		}else {
			System.out.printf("%d节点已存在,不能重复插入\n",node.no);
		}
		
	}
	
	//删除节点
	public void delete(int no) {
		//需要一个临时变量
		DoublePersonNode temp = head.next;
		//需要一个变量来判断是否找到删除的位置
		boolean flag = false;
		while(true) {
			if(temp == null) {
				//遍历结束,任然没找到
				break;
			}
			if(temp.no == no) {
				//找到
				flag = true;
				break;
			}
			temp = temp.next;
		}
		if(flag) {
			temp.pre.next = temp.next;
			if(temp.next != null) {
				temp.next.pre = temp.pre;
			}
			System.out.printf("已删除第%d节点\n",no);
		}else {
			System.out.printf("要删除的第%d节点不存在\n",no);
		}
	}
	
	//修改节点可变数据域部分的数据
	public void update(DoublePersonNode node) {
		//需要一个临时变量
		DoublePersonNode temp = head.next;
		//给个标志位判断是否找到
		boolean flag = false;
		while(true) {
			if(temp == null) {
				//遍历结束都没找到
				break;
			}
			if(temp.no == node.no) {
				//找到
				flag = true;
				break;
			}
			//后移
			temp = temp.next;
		}
		if(flag) {
			temp.name = node.name;
			temp.nickname = node.nickname;
		}else {
			System.out.printf("要修改的第%d号节点不存在\n",node.no);
		}
	}
	
	//查询显示节点
	public void show() {
		if(head.next == null) {
			//为空提示
			System.out.println("链表为空");
		}else {
			//需要一个临时变量
			DoublePersonNode temp = head.next;
			while(true) {
				if(temp == null) {
					break;
				}
				//输出
				System.out.println(temp);
				//后移
				temp = temp.next;
			}
		}
	}
	
}