目录
1.线性表的概念
2. 顺序表
2.1 概念及结构
2.2 接口的实现
3. 课后习题
4 全部代码总结
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见 的线性表:顺序表、链表、栈、队列、字符串....
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储 时,通常以数组和链式结构的形式存储。
线性表分为 顺序表 和 链表
数组就是:逻辑上连续,物理上也连续
链表就是:逻辑上连续,物理上非连续
什么是物理上连续?比如说一个整形数组,假设第一个元素的地址为 0x10,那么它后面开辟空间的地址都是0x14,0x18,0x1c....这样的顺序,一连串的地址空间。物理上连续说明在内存空间中是仅仅的挨在一起的空间
什么是逻辑上连续?比如说链表,虽然它们每个开辟出来的空间在内存中不一定是仅仅挨在一起的,但我存储了下一个空间的地址,下一个空间保存了下下个空间的地址,我可以根据这些地址把他们都找出来,虽然它们实际上并没有连续在一起,但在想象中是连在了一起。
逻辑连续可以想象成一条线连接接,而物理连续却是实打实的内存储存连续。
注释:不必纠结这说的是什么意思,到下面的代码实现你就会有体会。(我说的也不是很好)
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数 据的增删查改。
顺序表一般可以分为:
静态顺序表:使用定长数组存储。(没什么好讲的,就是平常使用的数组)
动态顺序表:使用动态开辟的数组存储。(需要讲解的内容)
相比之下动态顺序表更灵活, 根据需要动态的分配空间大小.
我们之前用的数组,最大的问题就是要先规定好这个数组的大小是多大,是定死的。比如类比现实生活中的手机通讯录。如果我是给手机设定通讯录大小的人,你说给通讯录设定大小为 10?还是1000?还是 1万呢?这根本就是不确定的,如果给的太小的话我们根本储存不了太多人的信息,但如果一开始我就直接给定范围是 1万空间,却又很占内存空间。这些我们就想要是能有一个数组能根据使用者的需要来开辟空间大小就好了。这样动态数组就产生了。
需要我们自己定义一个类,来拓展原本数组的功能。
我们来实现一个动态顺序表. 以下是需要支持的接口.
/**
* 接口:
* public void add(int val) //添加数据功能: add
* public void add(int index, int val)// 在 index 索引处 插入 val
* public int getByValue(int val) //1.查询当前动态数组中第一个值为val的元素对应的索引
* //若不存在,返回 -1,表示当前val不存在
* public boolean contains(int val) //2. 第二种,查询当前动态数组中是否包含值为val的元素,
* //若存在返回true,否则返回false
*
* public int get(int index) //3.第三种,查询当前动态数组中索引为index的元素值
*
* public int set(int index, int newValue)//1.修改 index 位置的元素值改为 newValue,并返回修改前的值
* public int setVal(int oldVal, int newValue) //2.修改第一个值为 oldVal的元素改为 newValue,并返回索引下标
*
* //1. 删除索引为 index 的元素,并返回删除前的元素
* public int remove(int index)
* //2. 头删 并返回删除的元素
* public int removeFirst()
*
* //3. 尾删除 并返回删除的元素
* public int removeLast()
*
* //4. 删除第一个为 val的元素,并返回是否删除成功
* public boolean removeValue(int val)
*
* //5. 删除数组中所有为 val的元素
* public void removeAllValue(int val)
*/
注释:你当然可以根据自己想要的功能设置你的代码,接口的名字也可以被修改。但是,方法的命名最好是有意义,杜绝以 aa,bb,abd;这样的方式去命名方法名。
代码实现:
1. 准备工作
/**
* 基于int的动态数组,根据元素的个数来动态调整数组大小
*/
public class MyArray {
// 存储的元素任然是int[]
private int[] data;
//存储元素的有效个数
private int size;
//无参构造函数 如果用户没有主动输出空间大小,我们就自定义一个
public MyArray(){
this(10);
}
//有参构造函数
public MyArray(int initCap){
this.data = new int[initCap];
}
}
注释:
size这个属性永远是当前有效元素的下一个位置的索引。(也可以理解为指向第一个还未存储或者是被使用的空间)
下—次再存储新的元素时候,直接使用size对应的索引即可。
//添加数据功能: add
public void add(int val){
data[size] = val;
size++;
//当数组添加元素时,有可能会占满数组
//所以这些我们需要判断是否需要扩容
if(size == data.length){
// 此时数组已满,扩容当前数组
grow();
}
}
/**
* 对于外部的使用者来说,压根都不知道你MyArray这个类中还有个int[],数组的扩容对
*外部也是不可见的
* 置为private权限
* 使数组增容,每次增加元素数组大小的两倍
* 使用 Arrays.copyOf --需要头文件引入
*/
private void grow() {
// copyOf方法返回扩容后的新数组
data = Arrays.copyOf(data, data.length * 2);
System.out.println("扩容成功");
}
注释:使用 Arrays.copyOf,需要在最上面引入类 import java.util.Arrays;
我们每完成一个功能最好就测试一下,但此时的 MyArray 是一个类,不能直接用 Arrays.toString() 方法来打印,但我们需要打印MyArray的对象时,需要我们拓展下toString方法。
/**
* 因为println打印 对象 时会自动调用toString方法
* 打印的却是地址,我们想要打印数组内容就要被toString方法
* 进行重写,重定义。
*/
//完成打印数组内容的功能 补充下toString的功能
public String toString(){
String rep = "[";
for (int i = 0; i < size; i++) {
// 此时取得是有效数据,使用size属性
// size表示当前有效元素的下一个索引位置
rep += data[i];
//最后一位元素需要 逗号
if(i != size - 1) {
rep += ", ";
}
}
rep += "]";
return rep;
}
创建一个用来测试的源文件,Test_MyArray
//测试是否能正常的插入元素
public class Test_MyArray {
public static void main(String[] args) {
//为了方便看效果,初始大小设为 3
MyArray arr = new MyArray(3);
arr.add(10);
arr.add(20);
System.out.println(arr);
arr.add(30);
System.out.println(arr);
arr.add(40);
System.out.println(arr);
}
}
运行结果:
在 index 索引处出插入 元素 val
怎么在数组中插入一个元素?
先画图理解,学数据结构画图是看家本领,必须要会的
其实只需要把 index处级后面的元素向后移动,就可以完成。
那么需要移动几步呢?size - 1 - index,有几个元素移动几个
/**
* 在 index 索引处 插入 val
*/
public void add(int index, int val){
//先判断给的索引值是否合理
if(index < 0 || index > size){
System.out.println("输入插入的索引值错误,请重新输入");
return;
}
// 从当前最后一个有效元素开始向后搬移元素,把index位置空出来
for (int i = size; i > index ; i--) {
data[i] = data[i - 1];
}
data[index] = val;
size ++;
//继续判断插入一个元素后是否需要扩容
if(size == data.length){
grow();
}
}
测试案例:
public class Test_MyArray {
public static void main(String[] args) {
//为了方便看效果,初始大小设为 3
MyArray arr = new MyArray(3);
arr.add(10);
arr.add(20);
System.out.println(arr);
arr.add(30);
System.out.println(arr);
arr.add(40);
System.out.println(arr);
arr.add(2, 5);
System.out.println(arr);
}
}
运行结果:
注释:
1. 为什么 index 可以去到 size?
因为当 index 等于 size的时候相当于 尾插
2. 因为我们程序是每次添加完一个数就进行判断数组是否满了,保证了每次添加数组时就是有未使用的空间的,所以当我取 data [size] 时就不会有数组越界的。不然的话移动前应该判断当前数组是否有空间能插入。
3. 数组的移动肯定是 从 后 向 前 进行后移的,如果是从前 向 后的话会覆盖原来的元素内容。
查询功能: 有三种情况
/**
* 查询当前动态数组中第一个值为val的元素对应的索引
* 若不存在,返回 -1,表示当前val不存在
*/
public int getByValue(int val){
for (int i = 0; i < size; i++) {
if(data[i] == val)
return i;
}
return -1;
}
//2. 第二种,查询当前动态数组中是否包含值为val的元素,若存在返回true,否则返回false
public boolean contains(int val){
if(getByValue(val) != -1){
return true;
}
return false;
}
//3.第三种,查询当前动态数组中索引为index的元素值
public int get(int index){
//先判断给的索引值是否合理
if(index < 0 || index >= size){
System.out.println("输入查询的索引值错误,请重新输入");
return - 1;
}
return data[index];
}
测试:
public class Test_MyArray {
public static void main(String[] args) {
//为了方便看效果,初始大小设为 3
MyArray arr = new MyArray(3);
arr.add(10);
arr.add(20);
System.out.println(arr);
arr.add(30);
System.out.println(20 + " 的索引下标为 " + arr.getByValue(20));
System.out.println("是否存在元素为20 " + arr.contains(20));
System.out.println("查询索引为2的元素 " + arr.get(2));
}
}
运行结果:
注释:查询的 index 就不能取到 size了,因为size指向的有效元素的下一个,查询size位置会越界
改:
/**
* 修改 index 位置的元素值改为 newValue,并返回修改前的值
*/
public int set(int index, int newValue){
//先判断给的索引值是否合理
if(index < 0 || index >= size){
System.out.println("输入修改的索引值错误,请重新输入");
return - 1;
}
int rep = data[index];//保存要修改的值
data[index] = newValue;
return rep;
}
/**
* 修改第一个值为 oldVal的元素改为 newValue,并返回索引下标
*/
public int setVal(int oldVal, int newValue){
for (int i = 0; i < size; i++) {
if(data[i] == oldVal){
data[i] = newValue;
return i;
}
}
System.out.println("没有这个元素存在,不能修改");
return -1;
}
测试:
public class Test_MyArray {
public static void main(String[] args) {
//为了方便看效果,初始大小设为 3
MyArray arr = new MyArray(3);
arr.add(10);
arr.add(20);
arr.add(30);
System.out.println(arr);
System.out.println(arr.set(0, 60));
System.out.println(arr);
System.out.println(arr.setVal(30, 50));
System.out.println(arr);
}
}
删除功能:
1. 删除索引为 index 的元素,并返回删除前的元素
//删除
/**
* 删除索引为 index 的元素,并返回删除前的元素
*/
public int remove(int index){
//先判断给的索引值是否合理
if(index < 0 || index >= size){
System.out.println("输入删除的索引值错误,请重新输入");
return - 1;
}
//判断如果里面没东西可以删除了,就不做改变
if(size <= 0)
return -1;
int rep = data[index];//保存要删除的元素值
//这里需要从 前 向 后依次前移
//移动次数是 size - 1 - index
//注释:size - 1是最大元素的索引
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
return rep;
}
注释:这一块删除的循环条件必须要弄懂
这里循环的条件为什么是 i < size - 1? 那么 i < size 行不行?
解析:必须要为 i < size - 1 ,我们必须要保证访问数组不能越界,所以要保证
i + 1 == size - 1; 那么 i 能取到的值最大为 i == size - 2,
条件就为 i <= size - 2 或者 i < size - 1
如果条件写成 i < size ,那么 i 的最小值为 size - 1,但 i + 1就会访问到有效元素的下一个位置,如果此时待删除的数组是满的,就会造成越界访问。
提示:我们这里写程序时是在元素插入后进行扩容判断的,保证了数组绝对不会满的情况,i可以取到size位置处。
但是!我们写程序不能抱着这样的心情去写,必须保持结果一定是对的,如果此时你的增加add是调用别人的方法,他把增容写在了添加前,这样也是合理的,可这样当i取到size 的位置处就会报错。
而且,虽然执行 i < size - 1 没有抹去最后一位元素的存在,但由于有 size--,在打印时就不会出现删除前的最后一位元素了,在数据结构里这叫逻辑删除,虽然物理上我们并没有真正的删除。况且这并不会影响程序的正常运转,当程序重新add添加时,还是会覆盖掉原来最后一位元素的位置。
测试:
public class Test_MyArray {
public static void main(String[] args) {
//为了方便看效果,初始大小设为 3
MyArray arr = new MyArray(3);
arr.add(10);
arr.add(20);
arr.add(30);
arr.add(40);
System.out.println(arr);
System.out.println(arr.remove(3));
System.out.println(arr);
System.out.println(arr.remove(0));
System.out.println(arr);
}
}
2. 头删和尾删就简单了
//2. 头删 并返回删除的元素
public int removeFirst(){
return remove(0);
}
//3. 尾删除 并返回删除的元素
public int removeLast(){
return remove(size - 1);
}
就是运用我们之前写的 remove 方法
3. 删除第一个为 val的元素,并返回是否删除成功
public boolean removeValue(int val){
int tmp = getByValue(val);
if(tmp != -1){
int rep = data[tmp];
remove(tmp);
return true;
}
System.out.println("不存在这个元素");
return false;
}
4. 删除数组所有为 val的元素
//5. 删除数组中所有为 val的元素
public void removeAllValue(int val){
for (int i = 0; i < size;) {
if(data[i] == val){
remove(i);
}else{
i++;
}
}
}
测试:
public class Test_MyArray {
public static void main(String[] args) {
//为了方便看效果,初始大小设为 3
MyArray arr = new MyArray(3);
arr.add(20);
arr.add(20);
arr.add(20);
arr.add(20);
System.out.println(arr);
arr.removeAllValue(20);
System.out.println(arr);
}
}
运行结果:
注释: 代码里的 删除 和 i++ 必须每次只能一个。因为但我们删除index位置的元素的,全部元素开始向前移,那么此时新的index位置处可能也是待删元素,如果直接 i++的话就跳过这个位置的元素。
到此为止,代码的增删改查的基础功能就完成了。
写完整个动态数组后可以去力扣写题目练练手。
27. 移除元素 - 力扣(LeetCode) (leetcode-cn.com)
26. 删除有序数组中的重复项
283. 移动零
点击题库,输入序列号就找到了。
import java.util.Arrays;
/**
* 基于int的动态数组,根据元素的个数来动态调整数组大小
*/
public class MyArray {
// 存储的元素任然是int[]
private int[] data;
//存储元素的有效个数
private int size;
//无参构造函数 如果用户没有主动输出空间大小,我们就自定义一个
public MyArray(){
this(10);
}
//有参构造函数
public MyArray(int initCap){
this.data = new int[initCap];
}
//添加数据功能: add
public void add(int val){
data[size] = val;
size++;
//当数组添加元素时,有可能会占满数组
//所以这些我们需要判断是否需要扩容
if(size == data.length){
// 此时数组已满,扩容当前数组
grow();
}
}
/**
* 对于外部的使用者来说,压根都不知道你MyArray这个类中还有个int[],数组的扩容对外部也是不可见的
* 置为private权限
* 使数组增容,每次增加元素数组大小的两倍
* 使用 Arrays.copyOf --需要头文件引入
*/
private void grow() {
// copyOf方法返回扩容后的新数组
data = Arrays.copyOf(data, data.length * 2);
System.out.println("扩容成功");
}
/**
* 在 index 索引处 插入 val
*/
public void add(int index, int val){
//先判断给的索引值是否合理
if(index < 0 || index > size){
System.out.println("输入插入的索引值错误,请重新输入");
return;
}
// 从当前最后一个有效元素开始向后搬移元素,把index位置空出来
for (int i = size; i > index ; i--) {
data[i] = data[i - 1];
}
data[index] = val;
size ++;
//继续判断插入一个元素后是否需要扩容
if(size == data.length){
grow();
}
}
/**
* 查询当前动态数组中第一个值为val的元素对应的索引
* 若不存在,返回 -1,表示当前val不存在
*/
public int getByValue(int val){
for (int i = 0; i < size; i++) {
if(data[i] == val)
return i;
}
return -1;
}
//2. 第二种,查询当前动态数组中是否包含值为val的元素,若存在返回true,否则返回false
public boolean contains(int val){
if(getByValue(val) != -1){
return true;
}
return false;
}
//3.第三种,查询当前动态数组中索引为index的元素值
public int get(int index){
//先判断给的索引值是否合理
if(index < 0 || index >= size){
System.out.println("输入查询的索引值错误,请重新输入");
return - 1;
}
return data[index];
}
/**
* 修改 index 位置的元素值改为 newValue,并返回修改前的值
*/
public int set(int index, int newValue){
//先判断给的索引值是否合理
if(index < 0 || index >= size){
System.out.println("输入修改的索引值错误,请重新输入");
return - 1;
}
int rep = data[index];//保存要修改的值
data[index] = newValue;
return rep;
}
/**
* 修改第一个值为 oldVal的元素改为 newValue,并返回索引下标
*/
public int setVal(int oldVal, int newValue){
for (int i = 0; i < size; i++) {
if(data[i] == oldVal){
data[i] = newValue;
return i;
}
}
System.out.println("没有这个元素存在,不能修改");
return -1;
}
//删除
/**
* 1. 删除索引为 index 的元素,并返回删除前的元素
*/
public int remove(int index){
//先判断给的索引值是否合理
if(index < 0 || index >= size){
System.out.println("输入删除的索引值错误,请重新输入");
return - 1;
}
//判断如果里面没东西可以删除了,就不做改变
if(size <= 0)
return -1;
int rep = data[index];//保存要删除的元素值
//这里需要从 前 向 后依次前移
//移动次数是 size - 1 - index
//注释:size - 1是最大元素的索引
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
size--;
return rep;
}
//2. 头删 并返回删除的元素
public int removeFirst(){
return remove(0);
}
//3. 尾删除 并返回删除的元素
public int removeLast(){
return remove(size - 1);
}
//4. 删除第一个为 val的元素,并返回是否删除成功
public boolean removeValue(int val){
int tmp = getByValue(val);
if(tmp != -1){
int rep = data[tmp];
remove(tmp);
return true;
}
System.out.println("不存在这个元素");
return false;
}
//5. 删除数组中所有为 val的元素
public void removeAllValue(int val){
for (int i = 0; i < size;) {
if(data[i] == val){
remove(i);
}else{
i++;
}
}
}
/**
* 因为println打印 对象 时会自动调用toString方法
* 打印的却是地址,我们想要打印数组内容就要被toString方法
* 进行重写,重定义。
*/
//完成打印数组内容的功能 补充下toString的功能
public String toString(){
String rep = "[";
for (int i = 0; i < size; i++) {
// 此时取得是有效数据,使用size属性
// size表示当前有效元素的下一个索引位置
rep += data[i];
//最后一位元素需要 逗号
if(i != size - 1) {
rep += ", ";
}
}
rep += "]";
return rep;
}
}