##线性表
###线性表是数据结构中最基本的一种结构,它是一个有限的有序序列(也允许为空),其元素具有相同的属性,且在逻辑关系上相邻的元素之间存在一对一的关系。
在线性表中,元素按照某种线性顺序进行排列,每个元素都有一个直接前驱和一个直接后继(除了第一个元素无前驱,最后一个元素无后继)。线性表可以分为两种主要的操作方式:
顺序存储结构(数组)和链式存储结构(链表)。
- 顺序存储结构(顺序表):线性表中的元素在计算机内存中按照其逻辑顺序依次存储,可以通过下标(索引)快速访问指定位置的元素。用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。
- 假设我们有一个包含元素(10, 20, 30, 40, 50)的顺序表,那么它在内存中的存储方式如下图所示(用方框代表数据元素,箭头表示连续存储):
+-----+-----+-----+-----+-----+ | 10 | 20 | 30 | 40 | 50 | +-----+-----+-----+-----+-----+ ^ ^ | | +-- 第一个元素 --+ + | 最后一个元素
每个方框代表一个存储单元,顺序表中的元素按照它们在表中的位置依次存放在这些连续的存储单元中。这样,我们可以通过下标(索引)快速访问任何一个元素,例如,索引为2的元素就是值为30的元素。
###模拟实现 : 顺序表的基本增删改查
在这里插入代码片 package override_seqlist; import start.pos_illegality; import java.util.Arrays; public class my_seqlist implements Ilist{ //重写所有抽象接口方法 //实例化对象 public int[] elem;//定义数组 顺序表的本质是列表数组 public int used_size;//初始化为 默认初始值为0 // public static final int DEFAULT_CAPACITY=10;//定义容量 // // //构造方法初始化 // public my_seqlist(){ // this.elem=new int[DEFAULT_CAPACITY]; // } public my_seqlist(int capacity){ this.elem=new int[capacity]; } //假设数组used_size=5 @Override public void add(int data) { // 新增元素,默认在数组最后新增 check_capacity();//检查容量是否满足 是否超载 this.elem[used_size]=data; this.used_size++; } private void check_capacity(){ if(is_full()){ //如果是满的话就进行特定方法的扩容 Arrays.copyOf(elem,elem.length*2); } } @Override public boolean is_full(){ if(elem.length==used_size){ return true; } return false;//判断数组中是否元素已经满载 } @Override public boolean is_empty(){ if(used_size==0){ return true;//判断是空 } return false;//判断数组是非空 } @Override public void add(int pos, int data) { try { check_pos(pos);//检查pos索引位置的合法性 }catch (pos_illegality e){ e.printStackTrace(); return; } check_capacity(); for(int i=used_size-1;i>=pos;i--){ elem[i+1]=elem[i]; }//将pos 后面的数据整体后移 elem[pos]=data; used_size++; } private void check_pos(int pos)throws pos_illegality{ if(pos<0||pos>this.used_size){ System.out.println("不合法!"); throw new pos_illegality("插入元素下标异常"+pos); //检查不合法后就抛出异常:自定义异常 } } @Override public boolean contains(int toFind) { //是否包含某个元素 //在这之前判断是不是空的 if(is_empty()){ return false; } for (int i=0;i<used_size;i++){ if(elem[i]==toFind){ return true; } } return false; } @Override public int indexOf(int toFind) { //找到某个元素对应的位置 if(is_empty()){ return -1; } for(int i=0; i<used_size;i++){ if(elem[i]==toFind){ return i; } } return -1;//在这种情况下,返回-1更为合适,因为这个函数的目的是找到某个元素对应的位置,而不是返回一个布尔值。使用-1作为未找到元素的标志更符合函数的语义,并且也更容易理解。如果使用返回false,可能会让人产生误解,以为函数返回的是一个布尔值,而不是元素的位置。 } @Override public int get(int pos) { checkonpos(pos); if(is_empty()){ return -1; //throw new my_new_try("获取指定下标出现的异常");//或者是抛出异常 } // for(int i=0;i<used_size;i++){ // if(i==pos){ // return elem[pos]; // } // }//多此一举 return elem[pos];//直接返回特定位置的元素值 } private void checkonpos(int pos) throws pos_illegality { if(pos<0||pos>=this.used_size){ System.out.println("不合法!"); throw new pos_illegality("获取指定下标元素下标异常"+pos); //检查不合法后就抛出异常:自定义异常 } }//检查pos合法性 @Override public void set(int pos, int value) { checkonpos(pos); elem[pos]=value; } @Override public void remove(int toRemove) { //删除的逻辑依然是转换位置 覆盖 int index=indexOf(toRemove);//找到元素的位置 if(index==-1){ return; } //在这段代码中,首先调用了indexOf方法来找到要移除的元素的位置。如果indexOf方法返回-1,即表示未找到要移除的元素,那么在接下来的代码中会执行`return;`语句。 //在Java中,`return;`语句用于结束当前方法的执行,并返回到调用该方法的地方。在这里,如果找不到要移除的元素,就会直接结束当前方法的执行,而不做任何其他操作。 for (int i=index;i<used_size-1;i++){ elem[i]=elem[i+1]; } used_size--;//指定位置的元素的添加和删除是同理 } @Override public int size() { return used_size;//数组大小即是used_size } @Override public void clear() { this.used_size=0;//实例对象this的used_size直接为零 则系统自动回收清空 } @Override public void display() { //打印 显示 for(int i=0;i<used_size;i++){ System.out.println(elem[i] + " ");//打印数组列表元素 } System.out.println();//换行 } }
###难点:在特定位置进行插入和删除 (逻辑相同:通过对数组后面的元素进行后移或者前移,完成对指定位置的插入与删除)!
###顺序表(Sequential List)作为线性表的一种存储结构,有以下优点和缺点:
优点:
-
随机访问:由于顺序表中的元素在内存中是连续存放的,可以通过下标(索引)直接访问任意位置的元素,因此查找、读取操作非常迅速,时间复杂度为O(1)。
-
插入和删除效率较高(特定情况):当进行插入或删除操作且是在表尾部时,只需要移动一个元素的位置或者不需要移动任何元素,所以操作时间复杂度为O(1)。
-
存储空间利用率高:如果线性表预先分配了足够的存储空间,那么顺序表的空间利用率可以达到很高,尤其是在数据量接近预分配空间时。
缺点:
-
插入和删除效率低(非特定情况):如果要在顺序表的中间或头部插入或删除元素,需要移动大量的元素来保持表的有序性,此时的时间复杂度会提升到O(n),n为表长。
-
存储空间动态分配困难:顺序表若要增加长度,必须申请新的连续内存空间并将原表元素复制过去,这个过程可能会比较耗时,并且在内存碎片严重的情况下可能无法获得足够大的连续空间。
-
表长受限:顺序表的长度在初始化时需要设定,如果设置过小,后续可能因为空间不足而无法添加更多元素;若设置过大,则可能导致空间浪费。
-
扩容成本高:随着表中元素数量的增长,有可能需要频繁地进行扩容操作,尤其是对于不确定大小的数据集,每次扩容都需要复制整个数组,这是相对耗时的操作。