数据结构作为一门基础学科;研究的是数据如何在计算机中进行组织和存储,使得我们可以高效的获取数据和修改数据.
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关.
数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。
数据的逻辑结构
指反映数据元素之间的逻辑关系的数据结构,其中的逻辑关系是指数据元素之间的前后间关系,而与他们在计算机中的存储位置无关。逻辑结构包括:
1.集合:数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系;
2.线性结构:数据结构中的元素存在一对一的相互关系;
3.树形结构:数据结构中的元素存在一对多的相互关系;
4.图形结构:数据结构中的元素存在多对多的相互关系。
数据结构可以分为三类:
程序=数据结构 + 算法
从数组开始
数组(Array)是有序的元素序列。 若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。 这些有序排列的同类数据元素的集合称为数组。
数组是用于储存多个相同类型数据的集合。
数组也是对象;长度给定后就不能变;数组分配的是连续空间
如果越界就会报错(索引值超过数组的索引 或者 索引值为负数时):
ArrayIndexOutOfBoundsException;
数组索引越界异常.
//数组的声明
数据类型[] 数组名;//常用的一种声明用法;
数据类型 数组名[];//另一种声明用法;
数组的创建方式
静态初始化
在创建时,就直接给数组中的元素直接赋值;
例如 int [ ] a={1,2,3,4};
动态初始化
在创建时,给了数组长度,没有给数组中的元素赋值;
例如: int [ ] a=new int [3];
当没有给定数组中元素时,不同的数据类型会有不同的默认值;
整数: 0
浮点数: 0.0
char: 空格
引用类型: null
布尔型:false
数组的索引从0开始计算,通过数组的索引可以快速找到数组中的元素.
索引可以有语义,也可以没有语义.
不是所有含有语义的数字都适合作为数组的索引;
而数组也可以处理没有索引没有语义的情况;(索引即是一个位置标记,数组作为存储数据的容器,那么里面的数据想放在什么位置就放在什么位置,此时索引即不存在语义问题)
这个数组不但可以使用泛型,存储任意类型的数据;还可以自动扩容;
/**
* 创建一个可变长度的数组;
* @date 2021/7/19/ 20:13
* @author 小智
*/
public class MyselfArray<T> {
/**
* data:数据容器;
*/
private T[] data;
/**
* size:数组中实际元素个数;
*/
private int size;
/**
* 有参构造;
* @param capacity 容量
*/
public MyselfArray(int capacity) {
//默认长度为0;
this.size = 0;
//将容量传入数组;
data = (T[]) new Object[capacity];
}
/**
* 无参构造;
*/
public MyselfArray() {
//默认容量为20;
this(20);
}
/**
* 判断数组是否为空;
* @return boolean (true:数组为空,false:数组不为空)
*/
public boolean isEmpty(){
return this.size == 0;
}
/**
* 获取数组中实际保存数据的个数;
* */
public int getSize() {
return size;
}
/**
*获取数组容量;
*/
public int getCapacity(){
return data.length;
}
/**
*调整数组容量大小;
* @param newCapacity 新的容量值;
*/
private void resize(int newCapacity) {
T[] newData = (T[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
/**
* 向数组指定位置添加元素的方法;
* @param element 元素
* @param index 索引值
* */
public void add(int index, T element) throws IllegalAccessException {
//对索引位置进行判断;
if (index < 0 || index > size) {
throw new IllegalAccessException("Index is error ! ");
}
//数组满了就自动扩容;
if (size == data.length) {
//新数组空间为原来的2倍;
resize(2 * data.length);}
//插入元素时,原来位置上的元素都要向后移动一位;
for (int i = size - 1; i >= index; i--) {
//将当前索引对应的元素赋给后一位索引位置的元素;
data[i + 1] = data[i];
}
data[index] = element;
//添加完成后;最终将数组的长度加一位;
this.size++;
}
/**
* 向数组的头部添加元素的方法;
* @param element 元素;
* */
public void addHead(T element) throws IllegalAccessException{
//调用向指定位置添加元素方法;索引值为0(即第一个位置);
add(0,element);
}
/**
* 向数组末尾添加元素的方法;
* @param element 元素
* */
public void addTail(T element) throws IllegalAccessException {
//调用向指定位置添加元素方法;索引值为长度size(末尾位置);
add(this.size,element);
}
/**
* 根据索引值;获取数组指定位置的元素的方法;
* @param index 索引值;
* @return T[index] 指定位置元素;
*/
public T getEleByIndex(int index) throws IllegalAccessException {
//首先对索引位置进行判断;
if (index < 0 || index > size - 1) {
throw new IllegalAccessException("Index is error ! ");
}
return data[index];
}
/**
* 根据索引值;修改指定位置的元素的方法;
* @param index 索引值;
* @param element 要修改的元素;
*/
public void setEleByIndex(int index, T element) throws IllegalAccessException {
//首先对索引位置进行判断;
if (index < 0 || index > size - 1) {
throw new IllegalAccessException("Index is error ! ");
}
//进行元素覆盖;
data[index] = element;
}
/**
* 获取数组头部元素的方法;
* @return T 数组第一个元素;
*/
public T getHead() throws IllegalAccessException {
return getEleByIndex(0);
}
/**
* 获取数组尾部元素的方法;
* @return T 数组最后一个元素;
*/
public T getTail() throws IllegalAccessException {
return getEleByIndex(size - 1);
}
/**
* 查询指定元素的索引值;
* @param element 指定的元素;
* @return index 索引值;
*/
public int getIndex(T element) {
//默认index为 -1;
int index = -1;
//进行遍历数组;
for (int i = 0; i < this.size; i++) {
if (data[i].equals(element) {
index = i;
break;
}
}
return index;
}
/**
* 判断数组是否包含指定的元素;
* @param element 指定的元素;
* @return boolean (true:包含;false:不包含)
*/
public boolean contains(T element) {
return getIndex(element) == -1 ? false : true;
}
/**
*输出数组;
* @return 数组
* */
@Override
public String toString() {
StringBuilder sbl = new StringBuilder();
sbl.append("[");
for (int i = 0; i < this.size; i++) {
sbl.append(data[i].toString());
//末尾之前的元素后面都加分隔符;
if (i != this.size - 1) {
sbl.append(",");
}
}
sbl.append("],数组的容量为:");
sbl.append( data.length).append("实际存放元素的个数为: ").append(size);
return sbl.toString();
}
/**
* 删除数组中指定位置元素的方法;
* @param index 指定索引位置
* @return T[index] 被删除的元素
*/
public T removeEle(int index) throws IllegalAccessException {
//首先对索引位置进行判断;
if (index < 0 || index > size - 1) {
throw new IllegalAccessException("Index is error ! ");
}
T result = data[index];
for (int i = index; i < size - 1; i++) {
//删除元素时,后面的元素要向前移动;
data[i] = data[i + 1];
}
//删除元素后,数组长度减一;
size--;
//当这个数组的实际元素个数小于容量的1/4时且数组的一半长度不为0时(即至少2位元素时);进行缩小容量;
if (size <= data.length / 4 && data.length / 2 != 0) {
resize(data.length / 2);
}
return result;
}
/**
* 删除指定的元素(首次出现的);返回索引值;
* @param element 指定的元素;
* @return index 被删除元素的索引值;
*/
public int removeEle(T element) throws IllegalAccessException {
//先根据元素找到索引;
int index = getIndex(element);
//根据索引值判断索引是否存在;
if (index != -1) {
//删除指定位置元素;
removeEle(index);
}
return index;
}
/**
* 删除头部元素;
* @return 数组第一个元素;
*/
public T removeHead() throws IllegalAccessException {
return removeEle(0);
}
/**
* 删除尾部元素
* @return 数组最后一个元素;
*/
public T removeTail() throws IllegalAccessException {
return removeEle(this.size - 1);
}
}
在算法中,使用复杂度分析代码的性能.
时间复杂度;
在进行算法分析时,语句总的执行次数 T(n) 是关于问题规模 n 的函数,进而分析 T(n) 随着 n 的变化情况并且确定 T(n) 的数量级。算法的时间复杂度,也就是算法的时间量度,即为 T(n) = O(f(n)) 。
表示随着问题规模 n 的增大,算法执行时间的增长率和 f(n) 的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中, f(n) 是问题规模n的某个函数.
用大写O( )体现时间复杂度.
O(1) : 常数阶| O(n) :线性阶| O(n²) : 平方阶| O(logn) :对数阶| O(nlogn) :线性对数|
大O描述的是算法的运行时间与输入数据之间的关系
n可以理解为输入的数据个数;
O(n) 在计算时间复杂度时,通常忽略常数. 实际时间T=c1*n+c2
常用的时间复杂度所需时间由小到大:
O(1)
对于创建的工具类数组进行时间复杂度分析;
在添加元素和删除元素时都涉及到了调整数组容量的resize方法;
该方法起到均摊复杂度的作用
该方法的时间复杂度为 O(n) ;
在添加元素时,需要扩容数组为原来的2倍;删除元素时,缩容为原来的1/2.
private void resize(int newCapacity) {
T[] newData = (T[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
数组添加元素:时间复杂度为O(n)
在向头部添加元素时,后面的元素都要向后移动位置
在向尾部添加元素时,其他位置的元素不移动
均摊复杂度
假设有个容量长度为4的数组,从尾部添加数组元素;前四步操作4次;第5步时,调用resize方法进行扩容(
扩容时要对原来的数组进行遍历,然后插到新数组中去
);那么第5步操作的实际操作为(4+1)=5次; 这5步操作实际上操作为9次;那么每一步所需要的时间 (9/5)约等于2;;这是尾部添加元素;时间复杂度:O(2);也就为常数阶O(1);
数组删除元素:时间复杂度为O(n)
在指定位置删除元素 O(n/2) = O(n)
在数组头部删除元素 O(n) ;在从头部删除元素时,后面的元素都要向前移动位置
在数组尾部删除元素 O(1);在从尾部删除元素时,其他位置的元素不移动
最坏的状态:时间复杂度都为O(n)
修改指定位置的元素: 时间复杂度为O(1)
仅执行那个指定位置的数据
查询元素
需要遍历数组
需要遍历数组
当出现同时进行增添一个尾部元素后又立即删除一个尾部元素;的状况;就是说调用一次添加方法,其中可能又进行一次扩容;调用resize方法;然后调用删除方法;其中还可能调用resize方法进行缩容;在这个过程中;数组的容量可能发生变化;时间复杂度也出现跳跃式的变化.
public class De {
public static void main(String[] args){
//新建自定义的数组工具类对象;
MyselfArray<Integer> arr=new MyselfArray();
//新建随机数对象;
Random random=new Random();
try {
//循环20次;
for (int i = 0; i <20; i++) {
//向数组末尾添加随机数;
arr.addTail(random.nextInt(100));
}
System.out.println("==========刚开始的容量及元素个数======");
System.out.println(arr);
System.out.println("==========第一次增,删后=============");
arr.addTail(23);
System.out.println(arr);
System.out.println(arr.removeEle(0));
System.out.println(arr);
System.out.println("==========第二次增,删后=============");
arr.addTail(23);
System.out.println(arr);
System.out.println(arr.removeEle(0));
System.out.println(arr);
System.out.println("==========第三次增,删后=============");
arr.addTail(23);
System.out.println(arr);
System.out.println(arr.removeEle(0));
System.out.println(arr);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
结果:数组的容量来回变化.
==========刚开始的容量及元素个数======
[5,34,44,0,63,1,45,3,18,29,86,73,18,51,0,80,18,76,32,83],数组的容量为:20实际存放元素的个数为: 20
==========第一次增,删后=============
[5,34,44,0,63,1,45,3,18,29,86,73,18,51,0,80,18,76,32,83,23],数组的容量为:40实际存放元素的个数为: 21
5
[34,44,0,63,1,45,3,18,29,86,73,18,51,0,80,18,76,32,83,23],数组的容量为:20实际存放元素的个数为: 20
==========第二次增,删后=============
[34,44,0,63,1,45,3,18,29,86,73,18,51,0,80,18,76,32,83,23,23],数组的容量为:40实际存放元素的个数为: 21
34
[44,0,63,1,45,3,18,29,86,73,18,51,0,80,18,76,32,83,23,23],数组的容量为:20实际存放元素的个数为: 20
==========第三次增,删后=============
[44,0,63,1,45,3,18,29,86,73,18,51,0,80,18,76,32,83,23,23,23],数组的容量为:40实际存放元素的个数为: 21
44
[0,63,1,45,3,18,29,86,73,18,51,0,80,18,76,32,83,23,23,23],数组的容量为:20实际存放元素的个数为: 20
也就是删除removeEle()元素方法和调整大小的 resize()方法过于着急;可以在删除元素方法中的缩小容量中更改条件;当数组的实际元素个数小于数组容量的1/4时进行数组缩容.
结果:经过三次的增删循环;容量并没有变化(由于数组实际元素个数没有达到小于容量的 1/4);
==========刚开始的容量及元素个数======
[28,40,73,34,36,47,44,22,90,73,42,21,72,6,26,54,66,50,54,70],数组的容量为:20实际存放元素的个数为: 20
==========第一次增,删后=============
[28,40,73,34,36,47,44,22,90,73,42,21,72,6,26,54,66,50,54,70,23],数组的容量为:40实际存放元素的个数为: 21
28
[40,73,34,36,47,44,22,90,73,42,21,72,6,26,54,66,50,54,70,23],数组的容量为:40实际存放元素的个数为: 20
==========第二次增,删后=============
[40,73,34,36,47,44,22,90,73,42,21,72,6,26,54,66,50,54,70,23,23],数组的容量为:40实际存放元素的个数为: 21
40
[73,34,36,47,44,22,90,73,42,21,72,6,26,54,66,50,54,70,23,23],数组的容量为:40实际存放元素的个数为: 20
==========第三次增,删后=============
[73,34,36,47,44,22,90,73,42,21,72,6,26,54,66,50,54,70,23,23,23],数组的容量为:40实际存放元素的个数为: 21
73
[34,36,47,44,22,90,73,42,21,72,6,26,54,66,50,54,70,23,23,23],数组的容量为:40实际存放元素的个数为: 20