Java数据结构与算法-线性表(详细实现)

前言

线性表(Linear List)
2.1 什么是线性表
  • 零个或多个数据元素的有限序列。

  • 非空线性表的逻辑结构特征

    • 有且仅有一个开始结点,无直接前趋,有且只有一个直接后继
    • 有且仅有一个结束结点,有且只有一个直接前趋,无直接前驱。
    • 内部结点都有且只有一个直接前趋和一个直接后继
    • 对于同一线性表,数据类型一直,数据元素长度一致。
2.2 线性表基本运算
  • 基本运算包括
    • initList
      • 初始化操作,建立一个空的线性表
    • listEmpty
      • 若线性表为空,返回true,否则返回false
    • clearList
      • 将线性表清空
    • getElem(index)
      • 将线性表中第index个位置的元素值返回
    • locateElem(data)
      • 在线性表中查找与data值相等的元素,查找成功返回该元素在线性表中的索引,否则返回-1
    • listInsert(index,data)
      • 在线性表中第index个位置插入data
    • listDelete(index)
      • 删除线性表第index个位置元素,返回该值
    • listLength
      • 返回线性表实际存储元素个数
    • getAll
      • 遍历线性表
  • 线性表两种存储结构对比
    • image-20210707140415254
2.3 顺序表(Sequential List)
  • 按照顺序存储方式存储的线性表。

  • 假设顺序表所有结点类型相同,每个节点占用的存储空间大小相同,每个结点占c个存储单位,设顺序表中开始结点a1的存储位置为LOC(a1),那么任意节点ai的存储地址LOC(ai)=LOC(a1)+(i-1)*c 1<=i<=n

  • 优点

    • 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
    • 可以随机读取任意位置的元素
  • 缺点

    • 插入或者删除,需要移动大量数据元素
    • 线性表长度变化较大时,难以确定存储空间的容量
    • 造成存储空间的碎片
  • 顺序表结构

    • Java代码设计

      package com.fc.sequentiallist;
      
      /**
       * @ClassName SequenceTable     线性表-顺序表结构
       * @Description 增加泛型,支持多种类型
       * @Author Fclever
       * @Date 2021/6/28 17:03
       **/
      public class SequenceTable<T> {
      
          /**
           * 顺序表的默认长度为10
           */
          public static final int MAXSIZE = 10;
      
          /**
           * 存储数据数组
           */
          Object[] data;
      
          /**
           * 保存顺序表实际存储数据元素个数
           */
          int length;
      
          public SequenceTable() {
          }
      
          /**
           * 1.初始化顺序表
           *      初始化操作,建立一个空的线性表
           */
          public void initList() {
              // 初始化数组
              this.data = new Object[this.MAXSIZE];
              if (this.data == null) {
                  throw new OutOfMemoryError();
              }
              // 设置长度
              this.length = 0;
          }
      
          /**
           * 2. 判断线性表是否为空
           *      若线性表为空,返回true,否则返回false
           * @return  结果
           */
          public boolean listEmpty() {
              return this.length == 0;
          }
      
          /**
           * 3. 将线性表清空
           */
          public void clearList() {
              for (int i=0;i<this.length;i++){
                  this.data[i] = null;
              }
          }
      
          /**
           * 4. 根据索引获取指定位置的数据元素
           *      获取元素只能获取0<=index<this.length范围元素
           * @param index 索引位置
           * @return 查询结果
           */
          public T getElem(int index) {
              // 判断索引是否合法
              if (index < 0 || index >= this.length)
                  // 抛出异常
                  throw new IndexOutOfBoundsException("index:"+index+",size:"+this.length);
              return (T) this.data[index];
          }
      
          /**
           * 5. 根据值查找索引位置,如果有多个,返回第一个检索到的位置
           *      返回-1表示查找不到
           * @param data 查找数据
           * @param <T>  泛型
           * @return 查询结果
           */
          public <T> int locateElem(T data) {
              for (int index = 0; index < this.length; index++) {
                  if (this.data[index].equals(data)) {
                      return index;
                  }
              }
              // 如果检索不到,则返回-1
              return -1;
          }
      
          /**
           * 6. 在指定索引位置插入数据元素
           *      判断容量和索引是否合法,然后移动元素,插入结点,长度增加
           *      插入元素只能插入0<=index<=this.lenght
           * @param index 索引
           * @param data  插入数据
           * @param <T>   泛型
           * @return 操作结果
           */
          public <T> void listInsert(int index, T data) {
              // 合法性校验顺序(先判容量,然后判断索引合法性),因为插入操作,容量是必须检验的,如果容量不够,就不需要检验索引
              // 判断容量是否足够,如果等于最大容量,则容量已满
              if (this.length >= this.MAXSIZE)
                  throw new OutOfMemoryError();
              // 判断索引是否合法
              if (index < 0 || index > this.length)
                  // 抛出异常
                  throw new IndexOutOfBoundsException("index:"+index+",size:"+this.length);
              // 元素后移
              for (int i = this.length; i> index;i--){
                  this.data[i] = this.data[i-1];
              }
              // 插入
              this.data[index] = data;
              // 长度增加
              this.length++;
          }
      
          /**
           * 7. 删除指定索引位置的元素,并返回该值
           *      判断顺序表是否为空,然后索引下标校验,移动元素,长度减一
           *      删除元素只能删除0<=index<this.length
           * @param index 索引位置
           * @return  被删除的元素
           */
          public T listDelete(int index) {
              // 判断索引是否合法
              if (index < 0 || index >= this.length)
                  // 抛出异常
                  throw new IndexOutOfBoundsException("index:"+index+",size:"+this.length);
              T data = (T) this.data[index];
              // 元素前移动
              for (int i=index;i<this.length-1;i++){
                  this.data[i] = this.data[i+1];
              }
              // 顺序表长度减一,然后将末尾元素置空
              this.length--;
              return data;
          }
      
          /**
           * 8. 获取线性表存储数据元素个数
           *
           * @return 数据元素个数
           */
          public int listLength() {
              return this.length;
          }
      
          /**
           * 9. 遍历元素
           */
          public void listAll() {
              for (int i=0;i<this.length;i++){
                  System.out.printf("第%d个元素是:%d\n",i+1,this.data[i]);
              }
          }
      }
      
    • 测试

      package com.fc.sequentiallist;
      
      import org.junit.Test;
      
      import static org.junit.Assert.*;
      
      /**
       * @ClassName SequenceTableTest     顺序表单元测试
       * @Description
       * @Author Fclever
       * @Date 2021/6/29 10:51
       **/
      public class SequenceTableTest {
      
      
          @Test
          public void testSequenceTable(){
              // 创建顺序表对象,初始化顺序表
              SequenceTable<Integer> sequenceTable = new SequenceTable<>();
              sequenceTable.initList();
              // 插入元素
              sequenceTable.listInsert(0,1);
              sequenceTable.listInsert(1,2);
              sequenceTable.listInsert(2,3);
              sequenceTable.listInsert(3,4);
              sequenceTable.listInsert(4,5);
              // 插入元素
              sequenceTable.listInsert(2, 11);
              // 删除元素
              sequenceTable.listDelete(3);
              // 遍历元素
              sequenceTable.listAll();
              // 查询长度
              System.out.println(sequenceTable.listLength());
              // 根据索引查询结点值
              System.out.printf("索引为4的结点值为:%d\n",sequenceTable.getElem(4));
              // 根据值查询所在索引
              System.out.printf("结点值为11的索引为:%d\n",sequenceTable.locateElem(11));
          }
      }
      
  • 顺序表的时间复杂度

    • 查询时间复杂度O(1)
    • 插入和删除
      • 最好情况,元素插入最后一个位置或者删除最优一个元素,时间复杂度为O(1),不需要移动元素
      • 最坏情况,插入第一个位置或者删除第一个元素,时间复杂度为O(n)
      • 平均来看,插入到第i个位置或者删除第i个元素,移动n-1个元素。根据概率原理,每个位置插入或者删除的元素可能性相同,所以平均移动次数和中间元素的移动次数相同为(n-1)/2
    • 整体来看,查询时间复杂度为O(1);插入和删除为O(n)。
2.4 链表(LinkedList)
  • 典型的链表结构中每个结点都包括数据部分和指针部分(数据部分保存结点的实际数据,指针部分保存下一个结点的地址)

  • 链表是一种动态存储分配的结构形式,可以根据需要动态申请所需要的内存单元。

  • 链表结构描述

    • 首先需要定义一个“头引用”变量(一般使用head表示,称为头指针),该引用变量指向链表结构的第一个结点(头结点),第一个结点的地址部分指向第二个结点…直到最后一个结点。最后一个结点不再指向其他结点,称为”表尾“,一般会在表尾的地址部分存放一个空地址null,链表至此结束。
    • image-20210629124746679
  • 头指针和头结点

    • 头指针
      • 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
      • 无论链表是否为空,头指针均不为空,头指针是链表的必要元素。
    • 头结点
      • 头结点是放在第一元素之前,数据域一般无意义。
      • 有了头结点,对第一个结点前插入结点和删除第一结点的操作就和其他结点统一了。
      • 头结点不一定是链表的必须要素。
  • 在单链表的插入和删除操作上,如果不知道第i个元素的位置,那么与线性表顺序存储结构的优势并不明显。但是,如果在第i个位置插入m个元素时,对于顺序存储结构,每次插入都需要移动n-i个元素,每次都是O(n);对于单链表,只有在第一次时,需要找到第i个位置的指针,是O(n),但是后面的多个元素的插入或者删除事件复杂度都是O(1)。

    • 对于插入和删除数据越频繁的操作,单链表的效率优势就越明显
  • 优点

    • 插入和删除,在找出某位置的指针之后,插入和删除的时间复杂度为O(1)
    • 单链表不需要分配存储空间,只有有就可以分配,元素个数也不受限制。
  • 缺点

    • 查找方面,时间复杂度为O(n)
  • 链表结构分类

    • 单链表:普通链式结构,每个结点只包含一个应用
    • 单循环链表:单链表的基础上,将终端结点的引用域null改为指向表头节点即可
    • 双向链表:每个结点包含两个引用,一个指向下一个结点,一个指向上一个结点
    • 双向循环链表:双向链表的基础上,终端结点的向下引用域指向表头结点,表头结点的向上引用域指向表尾结点。
    • 静态链表:使用数组描述的链表。
      • 数组第一个元素的cur存放备用链表的第一个结点的下标。
      • 数组的最后一个元素的cur用来存放第一个插入元素的下标,相当于头结点。
  • 单链表结构

    • Java

      • 结点
      package com.fc.linkedlist;
      
      /**
       * @ClassName Node  单链表中的结点
       * @Description
       * @Author Fclever
       * @Date 2021/6/30 10:45
       **/
      public class Node<T> {
      
          // 数据域
          T data;
      
          // 下一结点
          Node<T> next;
      
          // 构造方法
          public Node() {
              this.data = null;
              this.next = null;
          }
      
          public Node(T data, Node<T> next) {
              this.data = data;
              this.next = next;
          }
      }
      
      
      • 单链表
      package com.fc.list;
      
      
      /**
       * @ClassName LinkedList    线性表链表结构
       * @Description     增加泛型,支持多种结构
       * @Author Fclever
       * @Date 2021/6/29 10:59
       **/
      public class LinkedList<T> {
      
          // 头结点,始终指向第一个结点,不存储值
          private Node<T> head;
      
          // 单链表长度
          private int length;
      
          public LinkedList() {
          }
      
          /**
           * 1. 初始化
           */
          public void initList() {
              // 头结点为空
              this.head = new Node<>();
              this.length = 0;
          }
      
          /**
           * 2. 判断链表是否为空
           * @return  结果
           */
          public boolean listEmpty() {
              return this.head.next == null;
          }
      
          /**
           * 3. 头插法
           * @param data 待插入的数据
           */
          public void createListHead(T data){
              // 构建结点
              Node<T> node = new Node<>(data,null);
              // 判断分配内存是否成功
              if (node == null) {
                  throw new OutOfMemoryError();
              }
              // 插入
              node.next = this.head.next;
              this.head.next = node;
              // 长度增加
              this.length++;
          }
      
          /**
           * 4. 尾插法
           * @param data  结点数据
           */
          public void createListTail(T data) {
              // 构建待插入结点
              Node<T> node = new Node<>(data, null);
              // 判断分配内存是否足够
              if (node == null) {
                  throw new OutOfMemoryError();
              }
              /*
          * 代码优化转optimization1
          // 判断单链表是否为空
          if (this.head.next == null){
             // 为空直接插入
             this.head.next = node;
             return;
          }
          // 找到原链表最后一个结点
          Node<T> p = this.head.next;
          while (p.next != null) {
              p = p.next;
          }
          // 找到最后一个结点
          p.next = node;
          *
          */
              // optimization1:begin
              // 遍历查找最后结点
              Node<T> p = this.head;
              while (p.next != null){
                  p = p.next;
              }
              // 已找到最后结点
              p.next = node;
              // optimization1:end
              // 长度增加
              this.length++;
          }
      
          /**
           * 5. 清空链表
           *      Java没办法自己控制内存的回收,C的话可以采用该方法的思路实现
           */
          public void clearList() {
              // 注释部分逻辑有问题
      //        Node<T> p = this.head.next;
      //        while (p != null) {
      //            Node<T> q = p.next;
      //            p = null;
      //            p = q;
      //            q = q.next;
      //        }
      //        // 头指针指向null
      //        this.head.next = null;
      //        this.length = 0;
              //  head--->1---》1---》1---》1
              if (this.head.next == null) {
                  // 如果链表已经为空,直接返回
                  return;
              }
              Node<T> p = this.head.next;
              Node<T> q = p.next;
              while (q != null) {
                  p = null;
                  p = q;
                  q = q.next;
              }
              p = null;
              this.head.next = null;
              this.length = 0;
          }
      
          /**
           * 6. 获取单链表长度
           * @return  长度
           */
          public int getLength() {
              return this.length;
          }
      
          /**
           * 7. 根据值查找链表中结点
           * @param data  查找数据
           * @return  结点
           */
          public Node<T> findNode(T data){
              // 判断链表是否为空
              if (this.length == 0){
                  // 链表为空,返回null
                  return null;
              }
              // 遍历查找
              Node<T> p = this.head;
              while (p.next != null){
                  if (p.next.data.equals(data))
                      return p.next;
                  // 后移
                  p = p.next;
              }
              return null;
          }
      
          /**
           * 8. 插入结点
           * @param index 插入位置,索引从0开始
           * @param data  插入元素
           */
          public void insertList(int index,T data) {
              // 判断索引合法性,等于length相当于尾插
              if (index < 0 || index >= this.length) {
                  throw new IndexOutOfBoundsException("index:"+index+",size:"+this.length);
              }
              // 构建结点
              Node<T> node = new Node<>(data,null);
              // 判断分配内存是否成功
              if (node == null) {
                  throw new OutOfMemoryError();
              }
              // 遍历找到插入位置前一个结点
              Node<T> p = this.head;
              // 这里只需要针对index进行定位结点即可,因为开始已经对index做了合法性校验
              while (index > 0){
                  p = p.next;
                  index--;
              }
              // 已找到插入位置前一个元素
              node.next = p.next;
              p.next = node;
              // 长度增加
              this.length++;
          }
      
          /**
           * 9. 遍历元素
           */
          public void getAll() {
              Node<T> p = this.head;
              int index = 0;
              while (p.next != null) {
                  p = p.next;
                  System.out.printf("第%d个元素是%d\n", index++,p.data);
              }
          }
      
          /**
           * 10. 删除指定位置的结点
           * @param index 删除位置,从0开始
           */
          public void deleteList(int index){
              // 索引合法性校验
              if (index < 0 || index >= this.length) {
                  throw new IndexOutOfBoundsException("index:"+index+",size:"+this.length);
              }
              // 遍历查找删除前一个元素
              Node<T> p = this.head;
              while (index > 0){
                  p = p.next;
                  index--;
              }
              // 被删除结点
              Node<T> delete = p.next;
              // p已经指向删除元素的前一个元素,执行删除
              p.next = p.next.next;
              // 置空,方便垃圾回收删除,coder无法控制何时回收
              delete = null;
              // 长度减少
              this.length--;
          }
      }
      
    • 测试

      package com.fc.linkedlist;
      
      import org.junit.Test;
      
      import static org.junit.Assert.*;
      
      /**
       * @ClassName LinkedListTest
       * @Description
       * @Author Fclever
       * @Date 2021/6/30 10:54
       **/
      public class LinkedListTest {
      
          @Test
          public void testLinkedListTest(){
              // 初始化单链表
              LinkedList<Integer> linkedList = new LinkedList<>();
              linkedList.initList();
              // 尾插
              linkedList.createListTail(1);
              linkedList.createListTail(2);
              linkedList.createListTail(3);
              linkedList.createListTail(4);
              linkedList.getAll();
              // 获取长度
              System.out.println(linkedList.getLength());
              // 头插
              linkedList.createListHead(66);
              linkedList.createListHead(44);
              // 查找元素
              Node<Integer> node = linkedList.findNode(2);
              System.out.println(node.data);
              // 插入元素
              linkedList.insertList(3, 54);
              // 遍历元素
              linkedList.getAll();
              // 删除元素
              linkedList.deleteList(4);
              linkedList.deleteList(1);
              // 遍历
              linkedList.getAll();
          }
      }
      
  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
算法与数据结构涵盖了以下主要内容: 数据结构(Data Structures): 逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法: 算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法与数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值