阿里云【名师课堂】Java面向对象开发97 ~ 105:链表

97:链表基本概念

首先,就目前学习过的知识而言,如果想要保存多个对象,唯一能想到的就是对象数组
同时如果该数组可以保存任意的对象,那么可以想到的就是Object型的数组。

  • Object[] data = new Object[数组长度] ;

但是问题在于,数组是一个定长的线性结构,也就是说虽然对象数组的形式可以满足存放多个对象的需求。不过一旦我们的内容不足或者内容过多都有可能造成内存的浪费(数组空间没有充分利用、数组扩充都会造成内存空间浪费),而具体有多少内容是难以提前预测准确的。

  • 为了解决这个问题,最好的做法是不定义一个固定长度的数组,有多少数据就保存多少数据。就像一列火车,按需求动态增减车厢数量(数据项),内存允许的话可以无限制的更改存储数量。
    • 链表的示意图:
      在这里插入图片描述
  • 如果要想去定义这个火车车厢,不可能每节车厢存放的只有数据,还应该有一个指向,指向的是下一个结点。
  • 定义好节点之后,要知道真正存放的是数据,取出的时候一定需要将数据依次取出,这里要用递归的方式来完成。

范例:链表的基本雏形

class Node {  // 因为只有Node类可以在保存数据的同时设置节点(数据的先后关系)
	private Object data ;  // 当前真正要保存的数据
	private Node next ;  // 定义下一个节点
	public Node(Object data) {  // 车厢中保存数据
		this.data = data ;
	}
	public void setData(Object data) {
		this.data = data ;
	}
	public Object getData() {
		return this.data ;
	}
	public void setNext(Node next) {
		this.next = next ;
	}
	public Node getNext() {
		return this.next ;
	}
}

public class TestLinkDemo {
	public static void main(String args[]) throws Exception {
		// 1、封装几个节点
		Node root = new Node("火车头") ;  // 现在假设存放的数据是String型
		Node n1 = new Node("一号车厢") ;
		Node n2 = new Node("二号车厢") ;
		Node n3 = new Node("三号车厢") ;
		// 2、需要设置节点的关系(把车厢串起来)
		root.setNext(n1) ;
		n1.setNext(n2) ;
		n2.setNext(n3) ;
		// 3、输出节点
		print(root) ;
	}
	
	public static void print(Node node) {
		if (node != null) {  // 表示当前存在有节点
			System.out.println(node.getData()) ;
			print(node.getNext()) ;  // 继续向下取出
		}
	}
}

在这里插入图片描述
总结:在整个链表的实现过程之中,Node类的核心作用在于:保存数据和连接节点关系。但是以上的代码中比较麻烦,因为发现客户端(主方法)需要自己来进行节点的创建以及关系的配置。
所以所谓链表,就是需要有一个单独的类(假设名为Link)来实现Node的数据保存以及关系的配置。

98:链表实现结构说明

上节讲到的链表的实现是有巨大缺陷的,因为它需要使用者自己来处理数据的保存、数据的关系。所以为了更好地处理,应该追加一个程序类。假设这个类称为Link类。
下面是一个完整的链表设计结构:
在这里插入图片描述
但是目前可能还没有能力理解这种复杂程序,所以还是先学习简单的定义:

  • 使用私有的内部类可以保证只有Link类才有权限操作Node类。
    • 而且使用了内部类之后还有一个最明显的特点:内部类与外部类之间可以方便的进行私有属性的访问,也就是避免了setter方法和getter方法的冗杂的调用。
  • 链表的核心操作:增加、修改、删除、取出数据、判断某个数据是否存在······
class Link {  // 负责链表的操作
	// 定义为私有内部类,表示Node类只为Link类服务
	private class Node {  // Node类负责保存数据、设置节点
		private Object data ;  // 要保存的数据
		private Node next ;  // 定义下一个节点
		public Node(Object data) {  // Object类:保存任意类的对象
			this.data = data ;
		}
	}
	/* ---------下面定义真正的Link类------------ */
}

上面是链表中内部类的定义,链表具体功能的实现之后讲解。

99:增加链表数据

public void add(Object data) {}

想要在链表中实现一个数据的增加操作,应该在链表中提供一个追加的方法,而这个追加方法上的参数应该是Objec类。

1、在Link类中追加新的方法定义

	public void add(Object data) {
		if (data == null) {
			return ;
		}
	}

2、如何进行数据的保存?

  • 应该首先定义一个根节点,有了根节点之后才可以陆续添加后面的子节点。
    • 并且由于根节点的类型是Node,则一定在Link类中追加一个Node类的属性
private Node root ;

3、后续节点操作

如果想要进行数据的保存,那么必须将数据封装在Node节点类中,因为如果没有封装好就无法确认节点的先后顺序。

  • 数据本身不具备先后顺序的描述,数据只有保存在节点里面由节点进行顺序的描述。

在内部类add中添加:

		Node newNode = new Node(data) ;
		if (this.root == null) {  // 如果当前没有根节点
			this.root = newNode ;  // 把第一个节点设置为根节点
		} else {  // 如果根节点已经存在
			// 把此时的节点顺序的处理交给Node类自己完成
			this.root.addNode(newNode) ;
		}

在内部类Node中添加:

		// 第一次调用addNode:this = Link.root
		// 第二次调用addNode:this = Link.root.next
		// 第三次调用addNode:this = Link.root.next.next
		// ······
		public void addNode (Node newNode) {  // 处理节点关系
			if (this.next == null) {  // 当前节点下一个节点为空时。可以保存
				this.next = newNode ;
			} else {  // 当前节点的下一个节点不为空时
				this.next.addNode(newNode) ;  // 下一个节点再调用addNode,直到找到下一个节点为空
			}
		}

4、整体流程

https://github.com/colderThyKiss/LinkedList.git
在这里插入图片描述

100:取得链表数据个数

public int size() {}

在一个链表之中可以保存多个元素的数据,那么为了操作的方便就需要知道其一共保存的数据个数,所以需要有一个计数的统计操作。

1、在Link类中追加一个可以统计个数的属性count

private int count = 0 ;  // 当前的保存个数

2、在每次进行数据追加的时候都应该进行一个个数的累加

this.count++ ;

3、取得元素个数

可以直接取得元素的个数,定义方法:

	public int size() {  // 取得元素个数
		return this.count ;
	}

4、总体实现

https://github.com/colderThyKiss/LinkedList.git
在这里插入图片描述
可以看到,往Link中添加了三个数据后能够检测出数据的个数。

5、功能扩充:判断是否为空链表

public boolean isEmpty() {}

如果根元素为空,或者保存的元素个数为0,则称为空链表,则isEmpty()方法返回值应该为true。

  • 在Link类中add方法之后添加isEmpty方法:
	public boolean isEmpty() {
		return this.root == null && this.count == 0 ;
	}

调用:

		System.out.println(all.size() + " = " + all.isEmpty());

在这里插入图片描述

101:链表数据转换为对象数组

public Object[] toArray() {}

首先链表是一个动态对象数组,那么它当然也可以返回对象数组。但是要想进行数组的返回,首先必须要开辟一个数组(长度为上节中讲解的count),同时该数组内容的填充应该依次将节点中的数据取出才可以正常完成。
在这里插入图片描述

1、toArray()方法在Link类中

	public Object[] toArray() {}

2、对象数组的数据来自哪里

所有在返回的对象数组中的数据都在Node类中,那么就证明这些数据应该在Node类中利用递归来依次取出。那么应该在外部类(Link)中定义一个返回类型的属性。

	private Object[] reData ;  // 返回类型
	private int foot = 0 ;  // 操作脚标
	······
	public Object[] toArray() {
		if (this.root == null && this.count == 0) {
			return null ;
		} else {
			// 链表中有数据,则开辟相应长度的数组
			// 该数组一定要交给Node类进行处理
			this.reData = new Object[this.count] ;
			this.foot = 0 ;
			this.root.toArrayNode() ;
			return this.reData ;
		}
	}

3、在Node类中处理节点数据

		// 第一次调用toArrayNode:this = Link.root
		// 第二次调用toArrayNode:this = Link.root.next
		// 第三次调用toArrayNode:this = Link.root.next.next
		// ······
		public void toArrayNode () {  // 处理节点关系
			Link.this.reData[Link.this.foot++] = this.data ;
			if (this.next != null) {  // 现在还有下一个节点
				this.next.toArrayNode() ;
			}
		}

4、总体实现

https://github.com/colderThyKiss/LinkedList.git
在这里插入图片描述

102:查询数据

public boolean contains(Object data) {}

在链表之中存在许多数据,如果要想查询数据或判断某数据是否存在就可以用到contains方法。

1、首先判断链表是否有数据

如果链表是空,不需要进行后续迭代;如果不为空,进行数据的依次递归判断。考虑到标准化问题,这种操作需要equals()方法的支持。

		// 第一次调用containsNode:this = Link.root
		// 第二次调用containsNode:this = Link.root.next
		// 第三次调用containsNode:this = Link.root.next.next
		// ······
		public boolean containsNode(Object search) {
			if (search.equals(this.data)) {  // 找到了想查询的数据
				return true ;
			} else {
				if (this.next != null) {  // 当前节点之后还有其他节点
					return this.next.containsNode(search) ;
				} else {  // 当前节点是最后一个节点
					return false ;
				}
			}
		}

2、在Link类中应该确保要查找数据的合法性

	public boolean contains(Object search) {
		if (search == null || this.root == null) {  // 没有要查询的内容并且链表为空
			return false ;
		}
		return this.root.containsNode(search) ;
	}

3、总体实现

https://github.com/colderThyKiss/LinkedList.git
在这里插入图片描述
注意:查找需要equals()方法的支持,所以如果是自定义类一定要覆写equals()方法。

103:根据索引取得数据

public Object get(int index) {}

对于链表中的所有数据,严格来说都是有顺序的,增加顺序就是它的保存顺序。链表既然是动态数组,那么也应该有近似于数组的通过索引取得内容的操作。

1、在Node中追加:索引查找

		public Object getNode(int index) {
			if (Link.this.foot++ == index) {  // 找到了想查询的数据
				return this.data ;
			} else {
				this.next.getNode(index) ;
			}
			return null ;
		}

2、在Link中追加处理方法get

这个方法在执行时必须保证链表有数据。

	public Object get(int index) {
		if (index >= this.count) {  //  超过了保存的个数
			return null ;
		}
		this.foot = 0 ;
		return this.root.getNode(index) ;
	}

3、总体实现

https://github.com/colderThyKiss/LinkedList.git

		System.out.println(all.get(0)) ;
		System.out.println(all.get(3)) ;

在这里插入图片描述

104:修改指定索引数据

public void set(int index, Object newData) {}

该方法与上一节get方法类似,不过get方法是返回数据,而本set方法需要做一个数据的修改。

1、在Node中追加:索引查找、修改数据

		public void setNode(int index, Object newData) {
			if (Link.this.foot++ == index) {
				this.data = newData;
			} else {
				if (this.next != null) {
					this.next.setNode(index,newData) ;
				}
			}
		}

2、在Link中追加处理方法set

	public void set(int index, Object newData) {
		if (index >= this.count) {  //  超过了保存的个数
			return ;  // void没有返回值
		}
		this.foot = 0 ;
		this.root.setNode(index, newData) ;
	}

3、总体实现

https://github.com/colderThyKiss/LinkedList.git

		all.set(0, "HELLO") ;
		System.out.println(all.get(0)) ;

在这里插入图片描述

105:删除数据

public void remove(Object data) {}

如果要进行数据的删除处理,需要考虑两种情况:

  • 删除的是根节点
    • 若删除的是根节点,则意味着Link中的根节点的保存需要发生变化,该操作主要在Link中处理。
  • 删除的不是根节点
    • 所有操作交给Node类处理。

在这里插入图片描述

1、在Node中追加:删除的不是根节点的操作

		// 第一次调用removeNode:this = Link.root.next,previous = Link.root
		// 第二次调用removeNode:this = Link.root.next.next,previous = Link.root.next
		// ······
		public void removeNode(Node previous, Object data) {
			if (this.data.equals(data)) {  // 当前节点为要删除节点
				previous.next = this.next;  // 上一个节点指向当前节点
			} else {
				this.next.removeNode(this, data) ;
			}
		}

2、在Link中追加处理方法remove

	public void remove(Object data) {
		if (this.contains(data)) {  // 如果该数据存在则向下进行
			// 首先判断要删除的是否是根节点数据
			if (this.root.data.equals(data)) {  // root是Node类的对象,Node是Link的内部类,所以可以直接使用
				this.root = this.root.next ;  // 根节点变为下一个节点,代表原根节点被删除
			} else {
				this.root.next.removeNode(this.root, data) ;
			}
			this.count-- ;
		}
	}

3、总体实现

https://github.com/colderThyKiss/LinkedList.git

		all.remove("HELLO") ;
		all.remove("java") ;
		Object[] result1 = all.toArray() ;
		for (int i = 0 ; i < result1.length ; i++) {
			System.out.println(result1[i]) ;
		}

在这里插入图片描述

总结

至此,最简化链表才算完成,当然这只是一个最简单的单向链表,也没有考虑到性能问题。但是通过这个链表的学习,应该做到:清楚动态数组的实现原理。同时再次强调:contains和remove方法需要equals的支持。

序号 方法名 类型 功能
99 public void add(Object data) {} 普通 向链表中添加数据
100 public int size() {} 普通 取得元素个数
100 public boolean isEmpty() {} 普通 判断链表是否为空链表
101 public Object[] toArray() {} 普通 链表数据转换为对象数组
102 public boolean contains(Object data) {} 普通 查询数据或判断某数据是否存在
103 public Object get(int index) {} 普通 取得制定索引内容
104 public void set(int index, Object newData) {} 普通 修改指定索引对应的数据
105 public void remove(Object data) {} 普通 删除数据,需要equals支持
posted @ 2020-07-03 18:13  溺水的情书  阅读(197)  评论(0编辑  收藏  举报