线性结构-数组

news2024/10/7 10:21:31

数组(Array)是最简单的数据结构,是由有限个相同类型的变量或对象组成的有序集合。因为数组中各元素之间是按顺序线性排列的,所以数组是一种线性数据结构。

数组是一类物理空间和逻辑形式都连续的线性数据结构:

  • 数组用唯一的名字标识,通过数组名可以对数组中的元素进行引用。例如array[0]表示数组中的第一个元素。
  • 数组中的元素类型必须相同。
  • 数组的内存单元是连续的,一个数组要占据一个地址连续的内存空间。
  • 数组中的数据元素都是顺序存放的,元素之间有先后关系,数组元素之间不能存在空隙。

数组的定义

int[] array;
或者:
int array[];
这两种定义方式是等价的,不过第一种更符合Java的编程规范
上面只是声明了一个引用变量array,其本质还是一个指针,而数组本身并不存在,也就是说在内存中还没有开辟那段连续的存储空间。要使用数组,必须先对数组进行初始化。

Java中初始化数组有两种方法:静态初始化和动态初始化。

静态初始化:
定义数组时显式地指定数组的初始值,系统会根据初始值的个数和类型自动为数组在堆内存中开辟空间。

//定义数组和初始化数组同时完成
int[] array1 = {1, 2, 3};
int array2[] = {1, 2, 3};
//定义数组
int[] array3;
//初始化数组
array3 = new int[] {1,2,3};

动态初始化:
在初始化数组时仅指定数组的长度,不指定数组元素的初始值。
动态初始化不会显式地为数组指定初始值,系统为会该数组指定默认的初始值。

//定义数组和初始化数组同时完成
int[] array1 = new int[3];
int array2[] = new int[3];
//定义数组
int[] array3;
//动态初始化数组,只指定数组的长度
array3 = new int[3];
System.out.println(array3[0]);//0

定义自己的数组类

如果我们希望定义更加完备的数组结构,则可以定义一个数组类,对数组地属性和操作进行封装。

public class MyArray {
	int[] array;// 数组本身
	int elemNumber; // 记录数组中元素的个数

	public MyArray(int capacity) {
		array = new int[capacity]; // 动态初始化数组,长度为capacity
		elemNumber = 0;
	}

	public boolean insertElem(int elem, int index) {
		if (index < 1 || index > elemNumber + 1) {
			System.out.println("Insert index error ");
			return false;
		}

		if (elemNumber == array.length) {
			increaseCapacity();
		}

		// 循环地将第index个元素及后面的元素都向后移动一个位置
		for (int i = elemNumber - 1; i >= index - 1; i--) {
			array[i + 1] = array[i];
		}
		// 将新元素插入到腾出的array[index-1]
		array[index - 1] = elem;
		elemNumber++;
		return true;
	}

	public boolean deleteElem(int index) {
		// 删除数组中第index位置上的元素
		if (index < 1 || index > elemNumber) {
			System.out.println("Delete index error ");
			return false;
		}
		for (int i = index; i < elemNumber; i++) {
			array[i - 1] = array[i];
		}
		elemNumber--;
		return true;
	}

	public void increaseCapacity() {
		// 增加数组的容量
		// 初始化一个新数组,容量是array容量的1.5倍
		int[] arrayTmp = new int[array.length * 2];
		System.arraycopy(array, 0, arrayTmp, 0, array.length);
		array = arrayTmp;
	}

	public void printArray() {
		for (int i = 0; i < elemNumber; i++) {
			System.out.print(array[i] + " ");
		}
		System.out.println();
	}

	public static void main(String[] args) {
		MyArray array = new MyArray(5); // 初始化一个容量为5的数组
		array.insertElem(3, 1); // 在数组的第1个位置插入3
		array.insertElem(5, 2); // 在数组的第2个位置插入5
		array.insertElem(2, 3); // 在数组的第3个位置插入2
		array.insertElem(7, 4); // 在数组的第4个位置插入7
		array.insertElem(8, 5); // 在数组的第5个位置插入8
		array.printArray(); // 打印数组内容
		array.insertElem(0, 3); // 在数组的第3个位置上插入0,需要扩容
		array.printArray(); // 打印数组内容
		array.deleteElem(7); // 企图删除第7个元素,但是删除失败
		array.printArray(); // 打印数组内容
	}
}

Myarray是我们定义的数组类,该类中包含两个成员变量:

  • array表示一个int[]类型的数组,通过array[index]的形式可以引用到数组中的元素。
  • elemNumber表示数组中元素的数量。

需要注意数组的容量和数组中元素的数量之间的区别。

  • 数组的容量指数组在堆内存中开辟的内存单元的数量,也就是上述构造函数的参数capacity所指定的大小,它表示数组中最多可以存放多少个元素。
  • 数组的中元素的数量是变量elemNumber记录的数据,它表示该数组中当前存储的有效元素的数量。

我们可以通过array.length属性获取数组的容量,所以在Myarray类中不需要定义一个变量专门记录数组的容量,但是变量elemNumber是必须的,因为数组的容量与数组中元素的数量可能不相等,这就需要通过一个变量来记录数组中有效元素的数量,否则可能从数组中取出无效值。

向数组中插入元素

public boolean insertElem(int elem, int index)
这个函数的作用是在整型数组中的第index个位置上插入一个整型元素elem

首先要理解什么是数组的第index个位置以及什么是数组的第index个位置上插入元素


数组的第index个位置:

  • 数组中元素的位置是从1开始的,因此数组元素的下标与数组元素的位置相差1。这是一种约定俗成的规则,很多数据结构的书籍都是这样规定的。

数组的第index个位置上插入元素:

  • 就是插入的新元素要位于数组的第index个位置上,原index个位置上的元素以及后续元素都要顺序向后移动一个位置。

前面已经提到,数组的元素之间不能存在“空隙”,因此插入新元素的位置范围应为:[1,elemNumber+1],否则数组中出现空隙,从而无法判断哪些是无有效的元素、那些是无效的。

  1. 将数组中第index个及之后的元素都向后移动一个位置,将数组的第index个位置空出来。
  2. 将新元素插入数组的第index个位置,即array[index-1]=elem;因为数组的下标与数组的位置之间相差1,所以array[index-1]就是数组的第index个元素。

需要指出的是,如果插入元素的位置是elemNumber+1,也就是在数组的最后插入一个元素,则不需要执行移动数组元素的操作,直接将元素插入数组的elemNumber+1位置即可。

public boolean insertElem(int elem, int index) {
    if (index < 1 || index > elemNumber + 1) {
        System.out.println("Insert index error ");
        return false;
    }
    // 循环地将第index个元素及后面的元素都向后移动一个位置
    for (int i = elemNumber - 1; i >= index - 1; i--) {
        array[i + 1] = array[i];
    }
    // 将新元素插入到腾出的array[index-1]
    array[index - 1] = elem;
    elemNumber++;
    return true;
}

在这段代码中,如果函数insertElem()的参数index等于elemNumber+1,则代码中循环移动元素的操作实际上是不被执行的,因为循环变量i的初始值是elemNumber-1,不满足i≥index-1=elemNumber的循环条件。
最后不要忘记elemNumber++;

数组扩容

当数组元素数量达到数组的容量上限时,就不允许再向数组中插入新元素,而是直接返回false表示插入元素失败,但是这种方法限定了数组中元素的数量,不够灵活。

我们使用动态扩容方法解决数组容量问题。
当向数组中插入元素,而数组中的元素容量又达到上限时,可以调用一个数组扩容方法对数组进行扩容,这样数组的存储空间就会随着数组元素的增多而不断增大。

public void increaseCapacity() {
    // 增加数组的容量
    // 初始化一个新数组,容量是array容量的1.5倍
    int[] arrayTmp = new int[array.length * 2];
    System.arraycopy(array, 0, arrayTmp, 0, array.length);
    array = arrayTmp;
}

这里用到了System.arraycopy()函数,具体用法请参照上面的代码。

删除元素

public boolean deleteElem(int index)
这个过程与插入元素的过程正好相反,我们只需要将第index个位置之后的元素(不含第index个位置上的元素)顺序向前移动一个位置,并将数组元素的数量减一,就可以完成删除操作。
被删除数组元素的位置只能在[1,elemNumber]的范围内,删除其他位置的元素都是非法的。
这里区间的右端点与插入时不同,插入是**elemNumber+1**

public boolean deleteElem(int index) {
    // 删除数组中第index位置上的元素
    if (index < 1 || index > elemNumber) {
        System.out.println("Delete index error ");
        return false;
    }
    for (int i = index; i < elemNumber; i++) {
        array[i - 1] = array[i];
    }
    elemNumber--;
    return true;
}

数组的性能分析

数组适合读操作频繁,而插入删除操作较少的场景。在定义数组时,要根据实际需求指定数组大小。
如果需要扩容,则应该选择合适的扩容因子,既要尽量提高空间利用率,又要最大限度避免频繁扩容对数组性能的影响。

优点:

  • 数组是一种可随机访问的线性结构,只要给定数组名和数组的下标,就可以用 O ( 1 ) O(1) O(1)时间复杂度直接定位到对应的元素。

缺点:

  • 由于数组的元素都是顺序存储的,且数组元素之间不能存在空隙,因此在插入删除时会有大量元素移动,将严重影响效率。在数组中插入或删除一个元素的时间复杂度都是 O ( n ) O(n) O(n)级的。
  • 没有扩容功能的数组大小是固定的,在使用数组时容易出现越界的问题。增加了扩容功能的数组虽然能避免内存越界问题,但会导致内存资源的浪费,因为总有一些空闲的数组空间。

来道算法题

数组元素逆置

一个基础的双指针问题。


编写一个函数reverseArray(),将数组中元素逆置。例如原数组中的元素顺序是{1,2,3,4,5},那么逆置后数组中的元素顺序是{5,4,3,2,1}


数组元素的逆置操作一般要求不创建新数组,只在原数组内将数组元素的顺序颠倒过来,这样操作的效率比较高,实现起来也更加简单。
需要定义一个tmpElem作为数据缓冲区,同时要设置变量lowhigh,作为数组的下标分别指向数组的第1个元素和最后1个元素。然后执行以下步骤:

  1. low指向的元素和high指向的的元素通过临时变量tmpElem交换位置。
  2. 执行low++high--
  3. 重复上两个步骤直到low≥high

对于只有一个元素的数组,以上步骤可以将该元素原地逆置,结果也是正确的。
该算法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

public void reverseArray() {
    int tmpElem;
    for (int low = 1, high = elemNumber; low < high; low++, high--) {
        // 数据交换
        tmpElem = array[low - 1];
        array[low - 1] = array[high - 1];
        array[high - 1] = tmpElem;
    }
}

删除数组中的重复元素

方法很多的一道基础算法题


编写一个purge(),删除整数数组中重复的元素。例如,数组为{1,1,3,5,2,3,1,5,6,8},删除重复元素后数组变为{1,3,5,2,6,8}


三重循环 O ( n 3 ) O(n^3) O(n3)

解决这个问题,最直观的方法就是用三个循环:

  1. 首先用一个循环对每个数组元素进行定位。
  2. 在用一个循环,将第一层循环定位的元素,拿来逐个对比该元素之后的每个元素。
  3. 如果发现重复,则调用deleteElem()将该元素删除。deleteElem()本身就是用一重循环来进行删除操作的。
public void purge() {
    // 两层循环分别检索数组的每个元素
    for (int i = 1; i <= elemNumber; i++) {
        for (int j = i + 1; j <= elemNumber; j++) {
            if (array[i - 1] == array[j - 1]) {
                deleteElem(j - 1);
                // 由于deleteElem本身会将后面的元素提前,所以需要修正j的位置
                j--;
            }
        }
    }
}

优化删除步骤: O ( n 2 ) + O ( n ) = O ( n 2 ) O(n^2)+O(n)=O(n^2) O(n2)+O(n)=O(n2)

上面的算法简单直观,但时间复杂度很高,为 O ( n 3 ) O(n^3) O(n3)
我们可以在确定重复元素之后,不立刻删除该元素,而是等找到全部重复元素之后再进行整体删除。
这样就可以将 O ( n 2 ⋅ n ) = O ( n 3 ) O(n^2\cdot n)=O(n^3) O(n2n)=O(n3)变为 O ( n 2 ) + O ( n ) = O ( n 2 ) O(n^2)+O(n)=O(n^2) O(n2)+O(n)=O(n2)
从而优化整体的时间复杂度。

public void purge() {
    int flag = -111;
    int i, j, number = elemNumber;
    // 两层循环确定重复元素
    for (i = 1; i < elemNumber; i++) {
        for (j = i + 1; j < elemNumber; j++) {
            if (array[i - 1] == array[j - 1]) {
                // 将重复元素填充为标记值,此处为-111
                // 由于没有执行删除操作,也就没有元素前移,不需要修正j的位置
                array[j - 1] = flag;
            }
        }
    }
    // 找到第一个特殊标记flag
    for (i = 1; array[i - 1] != flag; i++)
        ;
    for (j = i + 1; j <= number;) {
        if (array[j - 1] != flag) {
            // 如果array[j-1]不等于flag,复制j所指的有效数据复制到i标记的位置
            // i和j中间的会增加一个无效数据,这个无效数据紧挨在i之后
            // 将i和j分别后移,i指向新的无效数据,j尝试检索下一个有效数据
            array[i - 1] = array[j - 1];
            i++;
            j++;
        } else {
            // 如果array[j-1]等于flag,则j后移,寻找下一个有效数据
            j++;
        }
        // i指向当前最后一个有效数据的下一个数据,将其-1,刷新为elemNumber
        elemNumber = i - 1;
    }
}

在上面的代码中,有二重循环+一重循环+一重循环。将一重循环单独拿出来,是为了优化时间复杂度。
涉及到数组的第index个位置。需要注意元素位置和元素下标的转换。

哈希表优化查找: O ( n ) + O ( n ) = O ( n ) O(n)+O(n)=O(n) O(n)+O(n)=O(n)

使用Hashset需要导入java.util.*
在向哈希表中添加新对象时,哈希表会判断重复对象。

  • 如果添加的对象与哈希表中已有对象重复,则添加失败,同时返回false。
  • 如果没有重复,则添加成功并返回true。

向哈希表中添加元素并查重的操作的时间复杂度仅为 O ( 1 ) O(1) O(1)

public void purge() {
int flag = -111;
int i, j, number = elemNumber;
//使用哈希表找出数组中的重复元素
HashSet<Integer>set=new HashSet<>();
for(i=1;i<=elemNumber;i++){
    if(!set.add(array[i-1])){
        array[i-1]=flag;
    }
}
// 找到第一个特殊标记flag
for (i = 1; array[i-1] != flag; i++)
    ;
for (j = i + 1; j <= number;) {
    if (array[j - 1] != flag) {
        // 如果array[j-1]不等于flag,复制j所指的有效数据复制到i标记的位置
        // i和j中间的会增加一个无效数据,这个无效数据紧挨在i之后
        // 将i和j分别后移,i指向新的无效数据,j尝试检索下一个有效数据
        array[i - 1] = array[j - 1];
        i++;
        j++;
    } else {
        // 如果array[j-1]等于flag,则j后移,寻找下一个有效数据
        j++;
    }
    // i指向当前最后一个有效数据的下一个数据,将其-1,刷新为elemNumber
    elemNumber = i-1;
}

}

这一算法的代价就是需要用到哈希表,增加了空间复杂度。以空间换时间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/507658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

输出数字的位数(C语言)以及逆序输出

#include <stdio.h>int main() {int N;int i 0;scanf("%d",&N);int a[5];int j;while(N > 0){a[i] N%10;i;N N/10;}printf("这个数字是%d位数\n",i); for(j 0;j < i;j){printf("%d",a[j]);} } 原题如下&#xff1a;

并发编程08:原子操作类

文章目录 8.1 基本类型原子类8.1.1 常用API简介8.1.2 Case 8.2 数组类型原子类8.2.1 常用API简介8.2.2 Case 8.3 引用类型原子类8.4 对象的属性修改原子类8.4.1 使用目的8.4.2 使用要求8.4.3 Case 8.5 原子操作增强类原理深度解析8.5.1 常用API8.5.2 面试题8.5.3 点赞计数器8.5…

读书笔记-《ON JAVA 中文版》-摘要15[第十五章 异常]

文章目录 第十五章 异常1. 异常概念2. 基本异常2.1 基本异常2.2 异常参数 3. 异常捕获3.1 try 语句块3.2 异常处理程序3.3 终止与恢复 4. 自定义异常4.1 自定义异常 5. 异常声明6. 异常捕获6.1 捕获所有异常6.2 多重捕获6.3 栈轨迹6.4 重新抛出异常6.5 精准的重新抛出异常 6.6 …

ExpressGridPack Crack快速电子表格

ExpressGridPack Crack快速电子表格 ExpressEditors库 外壳对话框-对话框窗体不会出现在活动监视器中。 TdxVisualRefinements.PPadding属性对dxTokenEdit没有影响。 Express库 TdxVisualRefinements.PPadding属性对dxTokenEdit没有影响。 ExpressQuantumTreeList套件 TcxTreeL…

二层交换机和三层交换机到底区别在哪?

你好&#xff0c;这里是网络技术联盟站。 今天我们谈谈二层交换机和三层交换机。 二层交换机的概念和特点 二层交换机是一种工作在数据链路层的网络设备&#xff0c;主要功能是根据数据帧中的MAC地址进行转发&#xff0c;并将这些MAC地址与对应的端口记录在自己内部的一个地…

IDA常用宏定义函数

一.引言 做题目遇到了几个神奇的函数. SDWORD1(x), SDWORD2(x), SHIDWORD(x) 通过查询得知是IDA的宏定义函数 宏定义本身类似字符串替换,假设#define x 666 只是编译器在预处理阶段进行宏展开,将所有的x替换为666,然后再进行编译 二.IDA宏定义头文件 可以在路径\IDA_Pro_7.7…

Dubbo2.7 纯注解使用+ Nacos + Springboot 整合集成

Dubbo2.7 纯注解使用 NacosSpringboot 环境准备篇相关依赖nacos准备代码编写服务提供者服务使用者整体结构图 结果 常规操作篇服务分组服务版本参数传递泛化调用参数校验只订阅延迟暴露服务端异步回调多协议复用多注册中心本地存根 服务治理篇超时时间重试并发控制权限控制服务…

css04笔记

目录 盒子模型 5.7 外边距折叠现象 – ① 合并现象 5.8 外边距折叠现象 – ② 塌陷现象 5.9 行内元素的margin和padding无效情况 一、结构伪类选择器 &#xff08;了解&#xff09;nth-of-type结构伪类选择器 二、伪元素 三、标准流 四、浮动 浮动的代码&#xff1a; …

用 Pygal 模拟掷骰子

这篇博客&#xff0c;我们将学习使用 python可视化包 Pygal 来生成矢量图形文件。针对于需要在尺寸不同的屏幕上显示的图表具有很大用处。因为它们可以自动缩放&#xff0c;以此来适合观看者的屏幕。 . 在这个项目中&#xff0c;我们将对掷骰子的结果进行分析。掷6面的常规骰子…

<Linux> 基础IO(文件操作、文件描述符fd、重定向)

基础IO&#xff08;文件操作、文件描述符fd、重定向&#xff09; 文章目录 基础IO&#xff08;文件操作、文件描述符fd、重定向&#xff09;一、回顾C和C的文件操作二、C语言文件IO1.什么是当前路径&#xff1f;2.C语言文件接口汇总3.默认打开的三个流 三、系统文件IO1.open2.c…

真题详解(索引长度计算)-软件设计(七十一)

真题详解(哈希冲突)-软件设计&#xff08;七十)https://blog.csdn.net/ke1ying/article/details/130566800 在面向对象系统中&#xff0c;一个类定义了大体相似的对象&#xff0c;这些对象共享_____。 属性和行为。 &#xff08;属性就是状态&#xff09; 数据库主要分为用户…

shapefile.js实现shp数据的上传与展示

概述 shapefile是常见的矢量数据格式&#xff0c;但是由于其文件组成结构很难在webgis上直接展示。本文通过express和compressing实现打包后shapefile文件的上传&#xff0c;并结合shapefile.js实现shapefile数据的转换展示。 实现效果 实现代码 1. 后端实现 router.post(/…

Android 引入hunter-debug监测代码运行时函数耗时和参数及返回值,Java(1)

Android 引入hunter-debug监测代码运行时函数耗时和参数及返回值&#xff0c;Java&#xff08;1&#xff09; &#xff08;1&#xff09;在工程的根build.gradle文件里面添加cn.quinnchen.hunter:hunter-debug-plugin引用&#xff1a; buildscript {repositories {mavenCentra…

SAP CAP篇三:定义Model

SAP CAP篇一:快速创建一个Service&#xff0c;基于Java的实现 SAP CAP篇二&#xff1a;为Service加上数据库支持 文章目录 理解CAP的ModelDomain-Driven DesignKISSBasic TypesCommon Reuse TypecuidmanagedtemporalCountry, Currency, LanguagecodeList Assocation & Comp…

匹配算法之 匈牙利算法详解

参考&#xff1a; 算法学习笔记(5)&#xff1a;匈牙利算法漫谈匈牙利算法匈牙利算法、KM算法匈牙利算法&#xff08;二分图&#xff09;通俗易懂小白入门&#xff09;二分图最大匹配——匈牙利算法多目标跟踪之数据关联&#xff08;匈牙利匹配算法和KM算法&#xff09;【小白学…

手把手教你使用gtest写单元测试

开源框架&#xff1a;gtest&#xff0c;它主要用于写单元测试&#xff0c;检查真自己的程序是否符合预期行为。这不是QA&#xff08;测试工程师&#xff09;才学的&#xff0c;也是每个优秀后端开发codoer的必备技能。 本期博文内容及使用的demo&#xff0c;参考&#xff1a; …

40、Java 并发编程基础 ①

目录 一、进程&#xff08;Process&#xff09;二、线程&#xff08;Thread&#xff09;三、线程的串行四、多线程五、多线程原理六、多线程优缺点七、Java 的默认线程八、开启新线程(1) new Thread()(2) 继承 Thread&#xff0c;重写 run 方法(3) run() 和 start() 九、多线程…

AutoCV第八课:3D基础

目录 3D基础前言1. nuScenes数据集2. nuScenes数据格式3. 点云可视化总结 3D基础 前言 手写 AI 推出的全新保姆级从零手写自动驾驶 CV 课程&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考。 本次课程主要学习点云数据的可视化。 课程大纲可看下面的思维导图。…

【Shiro】SimpleAuthenticationInfo如何验证password

一、前言 通篇的关键就是知道ShiroRealm类重写的doGetAuthenticationInfo这个方法&#xff0c;到底是谁的方法。 从上图我们可以知道&#xff0c;ShiroRealm最终继承到了AuthenticatingRealm这个方法。 二、自定义的ShiroRealm类 ps&#xff1a;该图中①上的注释是没看过底…

Jetpack之livedata原理

1.LiveData是什么&#xff1f; 只有在生命周期处于started和resumed时。livedata才会更新观察者 2.Livedata的各种使用方式 1.更新数据 class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceSta…