美文网首页
链表的定义与使用

链表的定义与使用

作者: 曾梦想仗剑天涯 | 来源:发表于2020-11-13 17:41 被阅读0次

链表实现简介

  • 链表的本质是一个动态的对象数组,它可以实现若干个对象的存储;
  • 在开发之中对象数组是一项非常实用的技术,并且利用其可以描述出“多”方的概念,例如:一个人有多本书,则在人的类里面一定要提供一个对象数组保存书的信息,但是传统的对象数组依赖于数组的概念,所以数组里面最大的确定在于:长度是固定的,正是因为如此所以在开发之中,传统的数组应用是非常有限的(数组的接收以及循环处理),但是如果要想进行灵活的数据保存,那么就必须自己来实现结构;
此图来源于李兴华老师
  • 传统对象数组的开发操作依赖于脚标(索引)的控制,如果要想实现内容的动态维护,难道太高来,复杂度也攀升来,现在就可以发现,对于一成不动的数据可以实用对象数组来实现,但是对于可能随时变化的数据就必须实现一个可以动态扩充的对象数组;
  • 所谓的链表实质性的本质是利用引用的逻辑关系来实现类似于数组的数据处理操作,以一种保存“多”方数据的形式,实现数组类似的功能;
此图来源于李兴华老师
  • 如果想要实现链表处理,那么需要一个公共的结构,这个结构可以实现数据的保存以及下一个连接的指向,为了描述这样的逻辑,可以把每一个存储理解为一个节点,所以此时应该准备出一个节点类,但是这个节点类里面可以保存各种数据类型的数据;
此图来源于李兴华老师
//直接操作Node很麻烦
class Node<E> {
  private E data;
  private Node next;
  public Node(E data) {
    this.data = data;
  }
  public void setNext(Node next) {
    this.next = next;
  }
  public E getData() {
    return this.data;
  }
  public Node getNext() {
    return this.next;
  }
}
public class LinkDemo {
  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类进行包装处理;
此图来源于李兴华老师

数据保存:public void add(E e)

  • 通过之前的分析可以发现进行链表操作的过程之中为了避免转型的异常应该使用泛型,同时也应该设计一个链表的标准接口,同时具体实现接口的时候还应该通过Node类做出节点的关系描述;
//基本结构
interface ILink<E> {    //设置泛型避免安全隐患
  public abstract 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;
    }
  }
  public void add(E e) {
  
  }
}
  • 在现在所定义的类之中并没有出现有setter和getter方法,是因为内部类的私有属性也方便外部类直接访问;
//实现数据增加
interface ILink<E> {    //设置泛型避免安全隐患
  public abstract 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);
      }
    }
  }
  //-------------------以下是ILink类中定义的成员
  private Node root;    //根元素
  //-------------------以下是ILink类中定义的方法
  public void add(E e) {
    if(e == null) {   //保存的数据为null
      return;   //方法调用直接结束
    }
    //数据本身并不具有关联特性,只有Node类有,那么想实现关联处理就必须将数据包装在Node类之中
    Node newNode = new Node(e);   //创建一个新的节点
    if(this.root == null) {   //现在没有根节点
      this.root = newNode;    //第一个节点作为根节点
    } else {
      this.root.addNode(newNode);//将新节点保存在合适的位置
    }
  }
}
public class LinkDemo {
  public static void main(String [] args) {
    ILink<String> all = new LinkImpl<String>();
    all.add("Hello");
    all.add("World");
  }
}
  • Link类只负责数据的操作和根节点的处理,而所有后续节点的处理全部都是由Node类负责完成的;

获取数据长度:public int size()

  • 在链表之中往往需要保存有大量的数据,那么这些数据往往需要进行数据个数的统计操作,所以应该在LinkImpl子类里面追加有数据统计信息,同时当增加或删除数据时都应该对个数进行修改;
  • 在ILink接口里面追加一个获取数据个数的方法;
public abstract int size();   //获取数据的个数
  • 在LinkImpl子类里面追加一个个数统计的属性;
private int count;
  • 在add()方法里面进行数据个数的追加;
public void add(E e) {
  if(e == null) {   //保存的数据为null
    return;   //方法调用直接结束
  }
  //数据本身并不具有关联特性,只有Node类有,那么想实现关联处理就必须将数据包装在Node类之中
  Node newNode = new Node(e);   //创建一个新的节点
  if(this.root == null) {   //现在没有根节点
    this.root = newNode;    //第一个节点作为根节点
  } else {
    this.root.addNode(newNode);//将新节点保存在合适的位置
  }
  this.count ++;
}
  • 需要在LinkImpl子类里面来返回数据的个数;
public int size() {
  return this.count;
}
  • 只是对于数据保存中的一个辅助功能;

空集合判断:public boolean isEmpty()

  • 链表里面可以保存有若干个数据,如果现在链表还没有保存数据,则就表示一个空集合,所以应该提供有一个是否是空集合判断;
  • 在ILink接口里面追加有判断方法;
public abstract boolean isEmpty();   //判断是否为空集合
  • 在LinkImpl里面覆写此方法
public boolean isEmpty() {
  //return this.root == null;
  return this.count == 0;
}
  • 使用根节点或者长度判断其本质是一样的;

返回集合数据:public Object [] toArray()

  • 链表本身属于一个动态对象数组,那么既然是对象数组,就可以把所有的数据以数组的形式返回,这个时候就可以定义一个toArray()方法,但是这个方法只能返回一个Object型的数组;
此图来源于李兴华老师
  • 在ILink接口里面追加新的处理方法;
public abstract Object [] toArray();    //将元素以数组的形式返回
  • 在LinkImpl子类里面追加两个属性;
private int foot;   //操作数组的脚标
private Object [] returnData;   //返回的数据保存
  • 在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();
  }
}
  • 在进行数据返回的时候一定要首先判断是否为空集合;
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 E get(int index)

  • 链表可以像数组一样进行处理,所以也应该可以像数组一样根据索引获取数据,在这样的情况下,就可以继续利用递归的形式来完成;
此图来源于李兴华老师
  • 在ILink接口里面追加新的方法;
public abstract E get(int index);   //根据索引获取数据
  • 在Node类里面追加有根据所以获取数据的处理;
public E getNode(int index) {
  if(LinkImpl.this.foot++ == index) {
    return this.data;
  } else {
    return this.next.getNode(index);
  }
}
  • 在LinkImpl子类里面定义数据获取的实现;
public E get(int index) {
    if(index >= this.count) {   //所以应该在指定范围之内
      return null;
    }
    this.foot = 0;    //重制索引的下标
    return this.root.getNode(index);
  }

*这一特点和数组是很相似的,但是需要注意的是,数组获取一个数据的时间复杂度为1,而链表获取数据的时间复杂度为n;

修改指定索引数据:public void set(int index, E data)

  • 根据索引获取了指定数据,那么就可以进行数据的修改;
  • 在ILink接口中追加有新的方法;
public void set(int index, E data)
  • 在Node类之中提供有数据修改的处理支持;
public void setNode(int index, E data) {
  if(LinkImpl.this.foot++ == index) {   //索引相同
    this.data = data;   //修改数据
  } else {
    this.next.setNode(index, data);
  }
}
  • 在LInkImpl子类里面进行方法覆写;
public void set(int index, E data) {
  if(index >= this.count) {
    return;
  }
  this.foot = 0;
  this.root.setNode(index, data);
}
  • 这种操作的时间复杂度也是n,因为依然需要进行数据的便利处理;

判断指定数据是否存在:public boolean contains(E data)

  • 在一个集合里面往往存在有大量的数据,有时候需要判断某个数据是否存在,这个时候就可以通过对象比较的模式(equals()方法)来完成判断;
  • 在ILink接口中追加判断的方法;
public abstract boolean contains(E data);   //判断数据是否存在
  • 在Node类中进行依次判断;
public boolean containsNode(E data) {
  if(data.equals(this.data)) {
    return true;
  } else {
    if(this.next == null) {
      return false;
    } else {
      return this.next.containsNode(data);
    }
  }
}
  • 在LinkImpl里面实现此方法;
public boolean contains(E data) {
  if(data == null) {
    return false;
  }
  return this.root.containsNode(data);
}
  • 由于整个链表没有null数据的存在,所以整体的程序在判断的时候直接使用每一个节点数据发出equals()方法调用即可;

数据的删除:public void remove(E data)

  • 数据删除指的是可以从集合里删除掉指定的一个数据内容,也就是说此时传递的是数据内容,那么如果要实现这种删除操作依然需要对象比较的支持,但是对于集合数据的删除需要考虑两种情况:
  • 要删除的是根节点数据(LinkImpl与根节点有关,所以这个判断由根节点完成):
此图来源于李兴华老师
  • 要删除的不是根节点数据(由Node类负责完成):
此图来源于李兴华老师
  • 在ILink接口里面追加新的删除方法;
public abstract void remove(E data);    //删除指定数据
  • 在LinkImpl子类里面实现根节点的判断;
public void remove(E data) {
  if(this.contains(data)) {   //判断数据是否存在
    if(this.root.data.equals(data)) {   //根节点为要删除的节点
      this.root = this.root.next;   //根节点的下一个节点
    }
    this.count--;
  }
}
  • 如果现在根节点并不是要删除的节点,那么就需要进行后续节点判断,但是请一定要记住此时根节点已经判断完成来,再判断应该从根节点的下一个节点开始判断,在Node类中追加删除处理;
public void removeNode(Node previous, E data) {
  if(data.equals(this.data)) {
    previous.next = this.next;    //空出当前节点
  } else {
    if(this.next != null) {   //有后续节点
      this.next.removeNode(this, data);   //向后继续删除
    }
  }
}
  • 完善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--;
  }
}
  • 删除的逻辑依靠的就是引用的改变处理完成的;

清空链表:public void clean()

  • 有些时候需要进行链表数据的清空,这个时候就可以直接根据根元素来进行控制,只要root设置为了null,那么后续的节点就都不存在了;
  • 在ILink接口里面追加有清空处理方法;
public abstract void clean();   //清空集合
  • 在LinkImpl子类里面覆写此方法
public void clean() {
  this.root = null;   //后续的所有节点都没了
  this.count = 0;   //个数清零
}
  • 这些就是链表的基本功能,当然,这只是一个最简单最基础的单向链表实现;
interface ILink<E> {    //设置泛型避免安全隐患
  public abstract void add(E e);    //增加数据
  public abstract int size();   //获取数据的个数
  public abstract boolean isEmpty();   //判断是否为空集合
  public abstract Object [] toArray();    //将元素以数组的形式返回
  public abstract E get(int index);   //根据索引获取数据
  public abstract void set(int index, E data);   //修改索引数据
  public abstract boolean contains(E data);   //判断数据是否存在
  public abstract void remove(E data);    //删除指定数据
  public abstract void clean();   //清空集合
}
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();
      }
    }
    public E getNode(int index) {
      if(LinkImpl.this.foot++ == index) {   //索引相同
        return this.data;   //返回当前数据
      } else {
        return this.next.getNode(index);
      }
    }
    public void setNode(int index, E data) {
      if(LinkImpl.this.foot++ == index) {   //索引相同
        this.data = data;   //修改数据
      } else {
        this.next.setNode(index, data);
      }
    }
    public boolean containsNode(E data) {
      if(data.equals(this.data)) {
        return true;
      } else {
        if(this.next == null) {
          return false;
        } else {
          return this.next.containsNode(data);
        }
      }
    }
    public void removeNode(Node previous, E data) {
      if(data.equals(this.data)) {
        previous.next = this.next;    //空出当前节点
      } else {
        if(this.next != null) {   //有后续节点
          this.next.removeNode(this, data);   //向后继续删除
        }
      }
    }
  }
  //-------------------以下是ILink类中定义的成员
  private Node root;    //根元素
  private int count;    //保存数据个数
  private int foot;   //操作数组的脚标
  private Object [] returnData;   //返回的数据保存
  //-------------------以下是ILink类中定义的方法
  public void add(E e) {
    if(e == null) {   //保存的数据为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 E get(int index) {
    if(index >= this.count) {   //所以应该在指定范围之内
      return null;
    }
    this.foot = 0;    //重制索引的下标
    return this.root.getNode(index);
  }
  public void set(int index, E data) {
    if(index >= this.count) {
      return;
    }
    this.foot = 0;
    this.root.setNode(index, data);
  }
  public boolean contains(E data) {
    if(data == null) {
      return false;
    }
    return this.root.containsNode(data);
  }
  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--;
    }
  }
  public void clean() {
    this.root = null;   //后续的所有节点都没了
    this.count = 0;   //个数清零
  }
}
public class LinkDemo {
  public static void main(String [] args) {
    ILink<String> all = new LinkImpl<String>();
    System.out.println("数据增加之前size = " + all.size() + "、是否为空集合:" + all.isEmpty());
    all.add("Hello");
    all.add("World");
    all.add("!");
    all.remove("World");
    all.clean();
    System.out.println("数据增加之后size = " + all.size() + "、是否为空集合:" + all.isEmpty());
    System.out.println("----------------输出集合数据--------------------");
    Object [] result = all.toArray();
    if(result != null) {
      for(Object obj : result) {
        System.out.println(obj);
      }
    }
    System.out.println("----------------根据索引获取数据----------------");
    System.out.println(all.get(0));
    System.out.println(all.get(1));
    System.out.println(all.get(2));
    System.out.println("----------------根据索引修改数据----------------");
    all.set(0, "你好");
    System.out.println(all.get(0));
    System.out.println(all.get(1));
    System.out.println("----------------判断数据是否存在----------------");
    System.out.println(all.contains("Hello"));
    System.out.println(all.contains("World"));
  }
}

相关文章

  • 链表的定义与使用

    1. 链表的基本形式 链表主要知识点 此次的内容属于引用部分的实际应用,所以需要依赖两点:依赖于引用传递this表...

  • 链表的定义与使用

    链表实现简介 链表的本质是一个动态的对象数组,它可以实现若干个对象的存储; 在开发之中对象数组是一项非常实用的技术...

  • 链表

    文章结构 链表的定义 链表的插入和删除操作 链表的特性 常见的链表结构 自定义链表 链表的经典操作 使用链表实现L...

  • 单链表

    单链表 单链表问题与思路 找出单链表的倒数第K个元素(仅允许遍历一遍链表)使用指针追赶的方法。定义两个指针fast...

  • 单链表反转的递归方法和非递归方法

    链表的节点可以定义为: 测试的输入数据,每次使用root作为方法的入参 使用循环来翻转单链表 思路 翻转单链表,其...

  • 循环链表定义及操作

    循环链表定义 定义与单链表一样,操作时将末结点的指针指向开始结点即可 循环链表操作 初始化循环链表 插入(尾插) ...

  • 算法-23.链表中环的入口结点

    一个链表中包含环,请找出该链表的环的入口节点。要求不能使用额外的空间。 思路:定义一个链表,从头结点开始执行,定义...

  • 数据结构_知识点_静态链表

    1. 静态链表定义 静态链表其实就是使用数组去代替指针以实现单链表。结点定义如下: 需要注意的是: 数组的第一个和...

  • LRU缓存算法

    1. LRU缓存可使用一个HashMap和双向链表实现 1.1定义双向链表的节点: 1.2实现:

  • 链表:反转链表

    定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。 1,使用栈解决 最简单的一种方式就是使用...

网友评论

      本文标题:链表的定义与使用

      本文链接:https://www.haomeiwen.com/subject/rxlbbktx.html