众所周知,数据结构和算法对于一个开发人员是多么的重要,一个好的数据结构和算法,可以让你在实现同一个功能的时候,提升非常多的效率。笔者作为一个初入IT业的菜鸟,觉得也很有必要在这方面下一番功夫,所以特开此篇作为学习数据结构和算法的开篇,后面会继续记录分享我的学习经历。
因为笔者主要做java这块儿的,所以前期写的东西可能都是以java来进行描述的,不过数据结构和算法这种东西,其实跟语言的关联性不是特别大。想学习算法,那么必须对数据结构有一定的了解,一些好的算法难免要结合一些数据结构的知识来实现。本篇讲述的是最基础最简单的数据结构(数组,集合和散列表),尽管它们很基础,但我们仍需要好好的去了解它们。
数组
数组是数据结构中最为基础的一个存储方式,是我们学习数据结构与算法的基石,事实上大部分的数据结构都可以用数组来进行实现。就像在前面java IO笔记的篇幅中,我们可以经常在源码中看到数组的存在,灵活的运用它们可以为我们解决很多事情。
那么数组是什么呢,在我看来数组就是有限个数的同类型数据,按照顺序排列,组合在一起的一个有序集合。当然在一些弱语言中,数组对内部元素的数据类型没有强制要求,如javascript中我们可以使用var来声明变量而不用指定数据类的类型,但在java中,数组要求数据元素必须是同一种类型。数组可以分为一维数组和多维数组,事实上多维数组就是一维数组的扩展,就是一维数组中的元素还是数组,如是而已。
数组有一个最为明显的特征,那就是它是定长的,在使用前我们需要先行明确它需要在内存中开辟出多大的空间,如果开辟的空间过小,那么我们无法将所需要的数据完整的存储到数组之中,如果我们开辟的过大,那么除了装载我们所需的数据之外,其它的空间将会被浪费掉。所以我们在使用数组的过程中必须要结合使用的场景谨慎使用,合理初始化。下面将结合一些java的代码以及图示来简单地描述数组的使用。
在java中,数组(一维)的创建方式如下,笔者为了方便,之后的实例中都将以int类型数组作为描述对象:
public class Test {
@org.junit.Test
public void initArray() {
// 该种方法先声明数组,然后为其赋值
int[] array1 = new int[5];
for (int i = 0; i < array1.length; i++) {
array1[i] = i + 1;
}
System.out.print("先声明后赋值: ");
printArray(array1);
// 该种方法在声明的同时就为其赋值
int[] array2 = new int[] { 1, 2, 3, 4, 5 };
System.out.print("声明同时直接赋值:");
printArray(array2);
}
private void printArray(int[] arrays) {
for (int temp : arrays) {
System.out.print(temp + " ");
}
System.out.println();
}
}
执行上述代码后可以在控制台看到如下打印:
上面展示的示例代码演示了在java中创建数组的过程。第一种是先声明再赋值,执行时,会先在内存中开辟出指定的空间,此时数组中没有赋值,jvm会自动为其在内存中赋上默认初始值,初始值根据数组类型来分配,在java中,引用型数据类型默认初始为null,基本数据类型的默认初始值如下图所示:
之后再将需要存储的值放入开辟好的空间之中,下面笔者将通过画图的方式更形象地来描述这个过程。
如上图所示,我截取了initArray方法中的一部分代码并画图分析。程序运行,首先jvm会将Test.class文件加载到方法区中,本例中,首先被压入栈内存的是initArray方法,方法中定义了一个局部变量array1,该变量是个int类型数组,初始化时,采用了先开辟空间后赋值的方式,因此当执行完第一条语句后,jvm会在堆空间中开辟出一个可以容纳5个int型数据的空间,此时数组空间中因为没有指定初始化值,jvm会自动向内填充初始值,赋值规则在上面已经提到过,不同类型数据的默认初始值是不同的,本例中,因为使用的是int型数组,所以默认的初始值为0,然后array1变量便指向了这片内存。之后通过一个for循环为数组重新赋值,堆空间中将新的值替换掉之前的默认值,接着执行printArray方法,该方法进栈,因为我们是将array1变量作为参数传入了该方法,所以该方法中的arrays参数也是指向着array1所在的这片内存,然后方法内通过一个循环遍历了该数组中的元素,并将它们打印了出来。
采用第二种方式其实在本质上跟第一种方式没有区别,都会经历默认初始化值这一步,只是给人的感官上是省略了默认初始化这一步。通过上面的描述,对数组有了初步的认识,我们知道了数组拥有着定长的特性,除了定长的特性,数组还有着顺序访问的特性,尽管在编程中我们可以使用索引来获取数组中指定位置的值,但计算机在执行时,其实还是一个一个的按顺序访问的,直到访问到指定索引的位置。
因为数组的特性,所以数组的适用场景主要是那些业务不会出现变化的场景。笔者曾经写过一个小的象棋游戏,当时的棋盘就是用二维数组来表示的,因为棋盘上可以落子的位置个数是固定的,正好符合数组的特性。尽管数组的作用很强大,但也有这其劣势,因为定长的特性,导致如果业务出现变化时,需要重新改变程序,所以下面就来说说数组的延伸运用。
动态数组
正如前面所说,数组虽然比较高效,但是因为其定长的特性导致了其应用场景的局限性。神说要有光,于是便有了光,因为有着需求,所以有了创新。为了解决数组定长的问题,集合这类数据结构诞生了,因为集合的概念比较宽泛,所以暂且将其理解为一个用于存储元素的容器,并且其是变长的。
集合的出现,解决了数组因为定长特性而不能解决的应用场景。集合跟数组一样也有着其一些属于自己的特性:
- 集合的容量是非固定的。当集合的当前容量不足以装载新的元素时,它会自动扩展容量以装纳新的元素。这点是跟数组最明显的区别。
- 集合中存放的为引用性数据类型,因为自动装箱拆箱的原因,也可以存放基本数据类型,但本质上在存储时基本数据类型已经自动装箱为其对应的引用型数据类型进行存储。
- 集合在不适用泛型约束的时候,一个集合中是可以存放多种类型的数据的。数组只能存放单一类型数据。
集合是一种比较宽泛的定义,为了解决各种不同的需求,它可以细化成很多独有的数据结构:
- 列表:列表是集合的一种,它是一种顺序结构,常见的有链表、队列、栈等。