课时134:链表实现简介
链表的本质是一个动态的对象数组,他可以实现若干个对象的存储。
这个讲的不太容易理解,容易睡着。但是都从第一课学到这了,不可能放弃重新学别的课程,每个老师的讲课方式不一样,既然选择了这个就一条路走到黑,把别的课程作为以后知识的巩固,推荐直接看B站的黑马程序员教程更容易理解,这一课的地址是:https://www.bilibili.com/video/BV1ZT4y1377x?p=3
-
链表的基本定义
在实际的开发之中,对象数组是一项非常实用的技术,利用其可以描述处“多”方的概念,例如:一个人有多本书,则在人的类里面一定要提供有一个对象数组保存书的信息,但是传统的对象数组依赖于数组的概念,所以数组里面最大的缺点在于:长度是固定的,正是因为如此所以在实际的开发之中,传统的数组应用是非常有限的(数组的接收以及循环处理),但是如果要想进行灵活的数组保存,那么必须自己来实现结构。
传统对象数组的开发操作依赖于角标(索引)的控制,如果要想实现内容的动态维护,那么难度太高了,而且复杂度攀升,所以现在就可以发现,对于一成不动的数据可以使用对象数组来实现,但是对于可能随时变化的数据就必须实现一个可以动态扩充的对象数组。
所谓的链表实质性的本质是利用引用的逻辑关系来实现类似于数组的数据处理操作,以一种保存“多”方数据的形式,实现数组类似的功能。
通过分析可以发现,如果现在想要实现链表处理,那么需要一个公共的结构 ,这个结构可以实现数据的保存以及下一个数据连接的指向,为了描述这样的逻辑,可以把每一个存储理解为一个节点,所以此时应该准备出一个节点类,但是这个节点类里面可以保存各种数据类型的数据。
虽然已经清楚了需要Node节点来进行数据的保存,但是毕竟这里面需要牵扯到节点的引用处理关系,那么这个引用处理关系是由使用者控制吗?这样肯定不可能,所以应该有一个专门的类来进行节点的引用关系的配置。
范例:直接操作Node很麻烦
class Node<E> {
private E data;
private Node next;
public Node(E data) {
this.data = data;
}
public E getData() {
return this.data;
}
public Node getNext() {
return this.next;
}
public void setNext(Node<E> next) {
this.next = next;
}
}
public class JavaDemo {
public static void main(String args[]) {
Node<String> n1 = new Node<String>("火车头");
Node<String> n2 = new Node<String>("车厢一");
Node<String> n3 = new Node<String>("车厢二");
Node<String> n4 = new Node<String>("车厢三");
Node<String> n5 = new Node<String>("车厢四");
n1.setNext(n2);
n2.setNext(n3);
n3.setNext(n4);
n4.setNext(n5);
print(n1);
}
public static void print(Node<?> node) {
if (node != null) { //有节点
System.out.println(node.getData());
print(node.getNext()); //递归调用
}
}
}
这样肯定不可能,所以应该有一个专门的类来进行节点的引用关系的配置。因为真实的使用者实际上关心的只是数据的存储与获取,所以现在应该对Node类进行包装处理。
课时135:数据增加
-
数据保存:public void add(E e)
通过之前的分析可以发现在进行链表操作的过程之中为了避免转型的异常应该使用泛型,同时也应该设计一个链表的标准接口,同时具体实现该接口的时候还应该通过Node类做出节点的关系描述。
范例:基本机构
interface ILink<E> { //设置泛型,避免安全隐患
public void add(E e);
}
class LinkImpl<E> implements ILink<E> {
private class Node { //保存节点的数据关系
private E data; //保存的数据
private Node next; //保存下一个引用
public Node(E data) { //有数据的情况下才有意义
this.data = data;
}
}
//------------以下为Link类中定义的结构------------
}
public class JavaDemo {
public static void main(String args[]) {
}
}
在现在所定义的Node类中并没有出现有setter与getter方法,是因为内部类中的私有属性也方便外部类直接访问。
范例:实现数据增加
interface ILink<E> { //设置泛型,避免安全隐患
public void add(E e);
}
class LinkImpl<E> implements ILink<E> {
private class Node { //保存节点的数据关系
private E data; //保存的数据
private Node next; //保存下一个引用
public Node(E data) { //有数据的情况下才有意义
this.data = data;
}
//第一次调用:this = LinkImpl.root;
//第二次调用:this = LinkImpl.root.next;
//第三次调用:this = LinkImpl.root.next.next;
public void addNode(Node newNode) { //保存新的Node数据
if (this.next == null) { //当前节点的下一个节点为null
this.next = newNode; //保存当前节点
} else {
this.next.addNode(newNode);
}
}
}
//------------以下为Link类中定义的成员-----------
private Node root; //保存根元素
//------------以下为Link类中定义的方法-----------
@Override
public void add(E e) {
if (e == null) { //保存数据为空
return; //方法调用直接结束
}
//数据本身是不具有关联特性的,只有Node类有,那么要想实现关联处理,就必须将数据包装在Node类之中
Node newNode = new Node(e); //创建一个新的节点
if (this.root == null) { //现在没有根节点
this.root = newNode; //第一个节点作为根节点
} else { //根节点存在
this.root.addNode(newNode); //将新节点保存在合适的位置
}
}
}
public class JavaDemo {
public static void main(String args[]) {
ILink<String> all = new LinkImpl<String>();
all.add("Hello");
all.add("World");
all.add("MLDN");
}
}
Link类只是负责数据的操作与根节点的处理,而所有后续节点的处理全部是由Node类负责完成的。
课时136:获取集合个数
-
获取数据长度:public int size()
在链表之中往往需要存大量的数据,这些数据需要进行数据个数的统计操作,所以应该在LinkImpl子类里面追加有数据统计信息吗,同时当增加或删除数据时都应该对个数进行修改。
1、 在ILink接口里面追加有一个获取数据个数的办法:
interface ILink<E> { //设置泛型,避免安全隐患
public void add(E e);
public int size(); //获取数据的个数
}
2、在LinkImpl子类里面追加一个个数统计属性,
|
3、在add()方法里面进行数据个数的追加:
public void add(E e) {
if (e == null) { //保存数据为空
return; //方法调用直接结束
}
//数据本身是不具有关联特性的,只有Node类有,那么要想实现关联处理,就必须将数据包装在Node类之中
Node newNode = new Node(e); //创建一个新的节点
if (this.root == null) { //现在没有根节点
this.root = newNode; //第一个节点作为根节点
} else { //根节点存在
this.root.addNode(newNode); //将新节点保存在合适的位置
}
this.count++;
}
4、在LinkImpl之类里面返回数据的个数
public int size(){
return this.count;
}
只是对于数据保存中的辅助功能。
课时137:空集合判断
-
空集合判断:public boolean isEmpty()
链表里面可以保存若干个空集合数据,如果说现在链表还没有保存数据,则就表示是一个空集合,,应该提供一个空的判断。
1、在Link里面追加有判断方法:
public boolean isEmpty(); //判断是否为空集合
2、在LinkImpl子类里面覆写此方法。
public boolean isEmpty() {
//return this.root == null;
return this.count == 0;
}
使用根节点或长度的判断其本质是一样的。
课时139:根据索引取得数据
-
返回集合数据:public Object[] toArray()
链表本身就属于一个动态对象数组,那么既然是一个对象数组,就可以把所有的数据以数组的形式返回,这个时候就可以定一个toArray()方法,但是这个时候的方法只能返回Object型的数组。
1、在ILink接口里面追加新的处理方法
public Object[] toArray; //将集合元素以数组的形式返回
2、在LinkIpml子类里面追加有两个属性:
private int foot = 0; //操作数组的角标
private Object[] returnData; //返回的数据保存
3、在Node类中递归获取数据
//第一次调用:this = LinkImpl.root
//第二次调用:this = LinkImpl.root.next
//第三次调用:this = LinkImpl.root.next.next
public void toArrayNode() {
LinkImpl.this.returnData[LinkImpl.this.foot++] = this.data;
if (this.next != null) { //还有下一个数据
this.next.toArrayNode();
}
}
4、在进行数据返回的时候一定要首先判断是否为空集合
public Object[] toArray() {
if (this.isEmpty()) { //空集合
return null; //现在没有数据
}
this.foot = 0; //角标清零
this.returnData = new Object[this.count]; //根据已有的长度开辟数组
this.root.toArrayNode(); //利用Node类进行递归数据
return this.returnData;
}
集合的数据一般如果要返回肯定要以对象数组的形式返回。
package cn.mldn;
interface ILink<E> { //设置泛型,避免安全隐患
public void add(E e);
public int size(); //获取数据的个数
public boolean isEmpty(); //判断是否为空集合
public Object[] toArray(); //将集合元素以数组的形式返回
public E get(int index); //根据索引获取数据
}
class LinkImpl<E> implements ILink<E> {
private class Node { //保存节点的数据关系
private E data; //保存的数据
private Node next; //保存下一个引用
public Node(E data) { //有数据的情况下才有意义
this.data = data;
}
//第一次调用:this = LinkImpl.root;
//第二次调用:this = LinkImpl.root.next;
//第三次调用:this = LinkImpl.root.next.next;
public void addNode(Node newNode) { //保存新的Node数据
if (this.next == null) { //当前节点的下一个节点为null
this.next = newNode; //保存当前节点
} else {
this.next.addNode(newNode);
}
}
//第一次调用:this = LinkImpl.root
//第二次调用:this = LinkImpl.root.next
//第三次调用:this = LinkImpl.root.next.next
public void toArrayNode() {
LinkImpl.this.returnData[LinkImpl.this.foot++] = this.data;
if (this.next != null) { //还有下一个数据
this.next.toArrayNode();
}
}
}
//------------以下为Link类中定义的成员-----------
private Node root; //保存根元素
private int count; //保存数据个数
private int foot = 0; //操作数组的角标
private Object[] returnData; //返回的数据保存
//------------以下为Link类中定义的方法-----------
@Override
public void add(E e) {
if (e == null) { //保存数据为空
return; //方法调用直接结束
}
//数据本身是不具有关联特性的,只有Node类有,那么要想实现关联处理,就必须将数据包装在Node类之中
Node newNode = new Node(e); //创建一个新的节点
if (this.root == null) { //现在没有根节点
this.root = newNode; //第一个节点作为根节点
} else { //根节点存在
this.root.addNode(newNode); //将新节点保存在合适的位置
}
this.count++;
}
public int size() {
return this.count;
}
public boolean isEmpty() {
//return this.root == null;
return this.count == 0;
}
public Object[] toArray() {
if (this.isEmpty()) { //空集合
return null; //现在没有数据
}
this.foot = 0; //角标清零
this.returnData = new Object[this.count]; //根据已有的长度开辟数组
this.root.toArrayNode(); //利用Node类进行递归数据
return this.returnData;
}
}
public class JavaDemo {
public static void main(String args[]) {
ILink<String> all = new LinkImpl<String>();
System.out.println("【增加前】数据的个数:" + all.size() + "、是否为空集合:" + all.isEmpty());
all.add("Hello");
all.add("World");
all.add("MLDN");
System.out.println("【增加后】数据的个数:" + all.size() + "、是否为空集合:" + all.isEmpty());
Object result[] = all.toArray();
for (Object obj : result) {
System.out.println(obj);
}
}
}
课时139:根据索引取得数据
-
获取指定索引数据:public E get (int index)
链表可以像数组一样进行处理,所以也应该可以像数组一样进行索引数据的获取,在这样的情况下就可以继续利用递归的形式来完成。
1、在ILink接口里面追加有新的方法
public E get(int index); //根据索引获取数据
2、在Node类里面追加有根据索引获取数据的处理
public E getNode(int index) {
if (LinkImpl.this.foot++ == index) { //索引相同
return this.data;
} else {
return this.next.getNode(index);
}
}
3、在LinkImpl子类里面定义数据获取的实现
public E get(int index) {
if (index >= this.count) { //索引应该在指定的范围之内
return null;
}// 索引数据的获取应该由Node类完成
this.foot = 0; //充值索引的下标
return this.root.getNode(index);
}
这一特点和数组很相似,但是需要注意的是,数组获取一个数据的时间复杂度为1,而链表获取数据的时间复杂度为n。
课时140:链表(修改指定索引数据)
-
修改指定索引数据:public void set(int index,E data)
现在已经可以根据索引来获取指定的数据了,但是既然可以获取数据,那么也可以进行数据的修改。
1、 在ILink接口中追加有新的方法
public void set(int index, E data); //修改索引数据
2、在Node类之中应该提供有数据修改的处理支持;
public void setNode(int index,E data){
if (LinkImpl.this.foot++ == index) { //索引相同
this.data = data; //修改数据
} else {
this.next.setNode(index,data);
}
}
3、在LinImpl子类里面进行方法的覆写
public void set(int index,E data){
if (index >= this.count) { //索引应该在指定的范围之内
return; //方法结束
}// 索引数据的获取应该由Node类完成
this.foot = 0; //充值索引的下标
this.root .setNode(index,data) ; //修改数据
}
这种操作的时间复杂度也是n,因为依然需要进行数据的遍历处理。
课时141:链表(判断数据是否存在)
-
判断指定数据是否存在:public boolean contains(E data)
在一个集合里面保存有大量的数据,有时候需要判断某个数据是否存在,就可以通过对象比较的方法(equals())来进行判断。
1、在ILink接口中追加追加判断的方法
public boolean contains(E data); //判断数据是否存在
2、在Node类中进行依次判断;
public boolean containsNode(E data) {
if (this.data.equals(data)) { //对象比较
return true;
} else {
if (this.next == null) { //没有后续节点了
return false; //找不到
} else {
return this.next.containsNode(data); //向后继续判断
}
}
}
3、在LinkImpl子类中实现此方法
public boolean contains(E data) {
if (data == null) {
return false; //没有数据
}
return this.root.containsNode(data); //交给Node类判断
}
由于整个的链表没有null数据的存在,所以整体的程序在判断的时候直接使用每一个节点数据发出equals()的方法调用即可。
课时142:链表(数据删除)
-
数据删除:public void remove(E data)
数据的删除指的是可以从集合里面删掉指定的一个数据内容,也就是说此时传递的是数据内容,如果要实现这种删除操作,依然需要对象比较的支持,但是对于集合数据的删除需要考虑两种情况:
- 要删除的是根节点数据(LinkImpl与根节点有关,所以这个判断由根节点完成):
- 要删除的不是根节点数据(由Node类负责):
1、在ILink接口里面追加新的删除方法
public void remove(E e); //数据删除
2、在LinkImpl子类里面实现根节点判断
public void remove(E data) {
if (this.contains(data)) { //判断数据是否存在
if (this.root.data.equals(data)) { //根节点为要删除节点
this.root = this.root.next; //根的下一个节点
}
this.count--;
}
}
3、如果现在根节点并不是要删除的节点,那么就要进行后续节点的判断了,此时根节点已经判断完成了,再判断应该从根节点的下一个开始判断,在Node类中追加删除处理。
public void removeNode(Node previous,E data){
if(this.data.equals(data)){
previous .next = this.next; //空出当前节点
}else{
if(this.next != null){ //有后续节点
this.next.removeNode(this,data) ; //向后继续删除
}
}
}
4、完善LinkImpl子类中的remove()方法
public void remove(E data) {
if (this.contains(data)) { //判断数据是否存在
if (this.root.data.equals(data)) { //根节点为要删除节点
this.root = this.root.next; //根的下一个节点
} else { //交由Node类进行删除
this.root.next.removeNode(this.root, data);
}
this.count--;
}
}
删除的逻辑依靠的是引用的改变处理完成的。
课时143:链表(清空链表)
-
清空链表:public void clean()
根据根元素进行链表的清空控制,只要root设置为了null,那么后续的节点就都不存在了。
1、在ILink接口里面追加有清空处理方法
public void clean(); //清空集合
2、在LinkImpl子类里面覆写方法
public void clean(){
this.root = null; //后续所有的节点都没了
this.count = 0; //个数清零
}
这些就是链表的基本功能,当然这只是一个最简单最基础的单向链表实现。
课时144:综合实战:宠物商店
假设有一个宠物商店,里面可以出售各种宠物,要求可以实现宠物的上架、下架,也可以根据关键字查询出宠物的信息。
1、定义出宠物的标准
interface Pet { //定义宠物标准
public String getName();
public String getColor();
}
2、宠物商店应该以宠物的标准为主;
class PetShop { //宠物商店
private ILink<Pet> allPets = new LinkImpl<Pet>(); //保存多个宠物信息
public void add(Pet pet) { //追加宠物,商品上架
this.allPets.add(pet); //集合中保存对象
}
public void delete(Pet pet) {
this.allPets.remove(pet);
}
public ILink<Pet> search(String key) {
ILink<Pet> searchResult = new LinkImpl<Pet>(); //保存查询结果
Object result[] = this.allPets.toArray(); //获取全部数据
if (result != null) {
for (Object obj : result) {
Pet pet = (Pet) obj;
if (pet.getName().contains(keyword) || pet.getColor().contains(keyword)) {
searchResult.add(pet); //保存查询结果
}
}
}
return searchResult;
}
}
3、根据宠物的标准定义宠物信息
定义宠物猫: | 定义宠物狗: |
| |
4、实现宠物商店的操作
package cn.mldn;
interface ILink<E> { //设置泛型,避免安全隐患
}
class LinkImpl<E> implements ILink<E> {
}
interface Pet { //定义宠物标准
public String getName();
public String getColor();
}
class PetShop { //宠物商店
private ILink<Pet> allPets = new LinkImpl<Pet>(); //保存多个宠物信息
public void add(Pet pet) { //追加宠物,商品上架
this.allPets.add(pet); //集合中保存对象
}
public void delete(Pet pet) {
this.allPets.remove(pet);
}
public ILink<Pet> search(String key) {
ILink<Pet> searchResult = new LinkImpl<Pet>(); //保存查询结果
Object result[] = this.allPets.toArray(); //获取全部数据
if (result != null) {
for (Object obj : result) {
Pet pet = (Pet) obj;
if (pet.getName().contains(keyword) || pet.getColor().contains(keyword)) {
searchResult.add(pet); //保存查询结果
}
}
}
return searchResult;
}
}
class Cat implements Pet { //实现宠物标准
private String name;
private String color;
public Cat(String name, String color) {
this.name = name;
this.color = color;
}
public String getName() {
return this.name;
}
public String getColor() {
return this.color;
}
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Cat)) {
return false;
}
if (this == obj) {
return true;
}
Cat cat = (Cat) obj;
return this.name.equals(cat.name) && this.color.equals(cat.color);
}
public String toString() {
return "【宠物猫】名字:" + this.name + "、颜色:" + this.color;
}
}
class Dog implements Pet { //实现宠物标准
private String name;
private String color;
public Dog(String name, String color) {
this.name = name;
this.color = color;
}
public String getName() {
return this.name;
}
public String getColor() {
return this.color;
}
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Dog)) {
return false;
}
if (this == obj) {
return true;
}
Dog dog = (Dog) obj;
return this.name.equals(dog.name) && this.color.equals(dog.color);
}
public String toString() {
return "【宠物狗】名字:" + this.name + "、颜色:" + this.color;
}
}
public class JavaDemo {
public static void main(String args[]) {
PetShop shop = new PetShop();
shop.add(new Dog("小黑狗","黑色") ) ;
shop.add(new Dog("小白狗","白色") ) ;
shop.add(new Cat("Tom","灰色") ) ;
Object result[]= shop.search("灰").toArray() ;
for(Object obj:result){
System.out.println(obj);
}
}
}
以上代码有误,add()什么的没实现
所有的程序开发都是以接口为标准进行的,这样在后期程序处理的时候就可以很灵活,只要符合标准的对象都可以保存。
课时145:综合实战:超市购物车
使用面向对象的概念表示出下面的生活场景:小明去超市买东西,所有卖到的东西都放在了购物车之后,最后到收银台一起结账。
1、定义出一个商品的标准:
interface IGoods{ //定义商品标准
public String getName();
public double getPrice();
}
2、定义购物车处理标准
interface IShopCar { //定义购物车
public void add(IGoods goods); //添加商品信息
public void delete(IGoods goods); //删除商品
public Object[] getAll(); //获得购物车中的全部商品信息
}
3、定义一个购物车的实现类
class ShopCarImpl implements IShopCar { //购物车
private LinkedList<IGoods> allGoodses = new LinkedList<IGoods>();
public void add(IGoods goods){
this.allGoodses.add(goods);
}
public void delete(IGoods goods){
this.allGoodses.remove(goods);
}
public Object[] getAll(){
return this.allGoodses.toArray() ;
}
}
4、定义一个收银台:
class Cashier { //收银台
private IShopCar shopcar;
public Cashier(IShopCar shopcar) {
this.shopcar = shopcar;
}
public double allPrice() { //计算总价
double all = 0.0;
Object result[] = this.shopcar.getAll();
for (Object obj : result) {
IGoods goods = (IGoods) obj;
all += goods.getPrice();
}
return all;
}
public int allcount() { //商品的数量
return this.shopcar.getAll().length;
}
}
5、定义商品信息:
图书: | 书包: |
| |
6、进行代码测试的编写
public class JavaDemo {
public static void main(String args[]) {
IShopCar car = new ShopCarImpl();
car.add(new Book("Java开发", 79.5));
car.add(new Book("Oracle", 79.5));
car.add(new Bag("登山包", 889.8));
Cashier cas = new Cashier(car);
System.out.println("总价格:" + cas.allPrice() + "、购买总数量:"+ cas.allcount());
}
}
整体的代码都是基于链表的功能实现的。
package cn.mldn;
import java.util.LinkedList;
interface IGoods { //定义商品标准
public String getName();
public double getPrice();
}
interface IShopCar { //定义购物车
public void add(IGoods goods); //添加商品信息
public void delete(IGoods goods); //删除商品
public Object[] getAll(); //获得购物车中的全部商品信息
}
class ShopCarImpl implements IShopCar { //购物车
private LinkedList<IGoods> allGoodses = new LinkedList<IGoods>();
public void add(IGoods goods) {
this.allGoodses.add(goods);
}
public void delete(IGoods goods) {
this.allGoodses.remove(goods);
}
public Object[] getAll() {
return this.allGoodses.toArray();
}
}
class Cashier { //收银台
private IShopCar shopcar;
public Cashier(IShopCar shopcar) {
this.shopcar = shopcar;
}
public double allPrice() { //计算总价
double all = 0.0;
Object result[] = this.shopcar.getAll();
for (Object obj : result) {
IGoods goods = (IGoods) obj;
all += goods.getPrice();
}
return all;
}
public int allcount() { //商品的数量
return this.shopcar.getAll().length;
}
}
class Book implements IGoods {
private String name;
private double price;
public Book(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return this.name;
}
public double getPrice() {
return this.price;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Book)) {
return false;
}
Book book = (Book) obj;
return this.name.equals(book.name) && this.price == book.price;
}
public String toString() {
return "【图书信息】名称:" + this.name + "、价格:" + this.price;
}
}
class Bag implements IGoods {
private String name;
private double price;
public Bag(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return this.name;
}
public double getPrice() {
return this.price;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Book)) {
return false;
}
Bag bag = (Bag) obj;
return this.name.equals(bag.name) && this.price == bag.price;
}
public String toString() {
return "【背包信息】名称:" + this.name + "、价格:" + this.price;
}
}
public class JavaDemo {
public static void main(String args[]) {
IShopCar car = new ShopCarImpl();
car.add(new Book("Java开发", 79.5));
car.add(new Book("Oracle", 79.5));
car.add(new Bag("登山包", 889.8));
Cashier cas = new Cashier(car);
System.out.println("总价格:" + cas.allPrice() + "、购买总数量:"+ cas.allcount());
}
}