1、数据结构
数据结构是计算机存储,组织数据的方式,指相互之间存在一种或多种特定关系数据元素的集合。
通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关
1.1、数据结构的基本功能
- 如何插入一条新的数据项
- 如何寻找某一特定的数据项
- 如何删除某一特定的数据项
- 如何迭代的访问各数据项,以便进行显示或其他操作
1.2、常用的数据结构
- 数组Array
- 栈Stack
- 队列Queue
- 链表Linkde List
- 数Tree
- 哈希表Hash
- 堆Heap
- 图Graph
2、算法
算计简单来说就是解决问题的步骤
在Java中,算法通常都是由类的方法来实现的。前面的数据结构,比如链表为啥插入、删除块,而查找慢,平衡的二叉树插入、删除、查找都快,这都是实现这些数据结构的算法所造成的。
2.1、算法的五个特征
- 有穷性:对于任意一组合法输入值,在执行有穷步骤之后一定能结束,即:算法中的每个步骤都能在有限时间内完成。
- 确定性:在每种情况下所应执行的操作,在算法中都有确定的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件下,算法都只有一条执行路径。
- 可行性:算法中的所有操作都必须足够基本,都可以通过已经实现的基本操作运算有限次实现。
- 有输入:作为算法加工对象的量值,通常体现在算法当中的一组变量。有些输入量需要在算法执行的过程中输入,而有的算法表面上可以没有输入,实际上已被嵌入算法之中。
- 有输出:它是一组与输入有确定关系的量值,是算法进行信息加工后得到的结果,这种确定关系即为算法功能。
2.2、算法的设计原则
- 正确性:首先,算法应当满足以特定的规则说明方式给出的需求。其次,对算法是否正确的理解有以下四个层次:
一、程序语法错误
二、程序对于几组输入数据能够得出满足需求的结果
三、程序对于精心选择的、典型、苛刻且带有刁难性的几组输入数据能够得出满足要求的结果
四、程序对于一切合法的输入数据都能得到满足要求的结果 - 可读性:算法为了人的阅读与交流,其次才是计算机执行。因此算法应该易于人的理解。另一方面,晦涩难懂的程序易于隐藏较多的错误而难以调试
- 健壮性:当输入的数据非法时,算法应当恰当的做出反应或进行相应处理,而不是产生莫名其妙的输出结果。并且处理出错的方法不应是中断程序执行,而是应当返回一个表示错误或错误性质的值,以便在更高的抽象层次进行处理
- 高效率与低存储量需求:通常算法效率指的是算法执行时间;存储量是指算法执行过程中所需要的最大存储空间,两者都与问题的规模有关。
前面三点正确性,可读性和健壮性都好理解。对于第四点算法的执行效率和存储量,我们知道比较算法的时候,可能会说‘A算法比B算法快两倍’之类的话,但实际上这种说法没有任何意义。因为当数据项个数发生变化时,A算法和B算法的效率比例也会发生变化,比如数据项增加了50%,可能A算法比B算法快三倍,但是如果数据项减少了50%,可能A算法和B算法速度一样。所有描述算法的速度必须要和数据项的个数联系起来。
3、数组
3.1、Java数组介绍
在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型(Object类型除外)
数组的声明
第一种方式:
数据类型[] 数组名 = new 数据类型[数组长度];
这里[]可以放在数组名称的前面,也可以放在数组名称的后面,我们推荐放在数组名称的前面,这样看上去数据类型[] 表示的很明显是一个数组类型,放在数组名称之后则显得不是那么直观
第二种方式:
数据类型[] 数组名称 = {元素1,元素2};
这种方式声明数组的同时直接给定了数组的元素,数组的大小由给定的数组元素个数决定
//声明数组1,长度为1,存放int类型的数组
int[] array1 = new int[1];
//声明数组2,长度为3,数组元素为1,2,3的数组
int[] array2 = new {1,2,3};
访问数组元素以及给数组元素赋值
数组是存在下标索引的,通过下标可以获取指定位置的元素,数组索引是从0开始的,也就是说索引0对应的就是数组中第一个元素,可以很方便的对数组中的元素进行存取操作。
前面的数组的声明第二种方式,我们在声明数组的同时,也进行了初始化赋值
//声明一个长度为3,存放int类型的数组
int[] array1 = new int[3];
//给array1第一个元素赋值1
array1[0] = 1;
/访问array1的第一个元素
System.out.println(array1[0]);
上面的array1数组,我们只能赋值三个元素,也就是索引从0到2,如果你访问array1[3],那么会报数组越界异常
java.lang.ArrayIndexOutOfBoundsException
数组遍历
数组有个length属性,是记录数组的长度的,我们可以利用length属性来遍历数组
//声明一个元素为1,2,3的数组
int[] array = new {1,2,3};
//遍历数组
for( int i = 0 ; i < array.length ; i++) {
System.out.println(array[i]);
}
3.2、用类封装数组实现数据结构
前面我们介绍了一个数据结构必须具有以下的基本功能
- 1.如何插入一条新的数据项
- 2.如何寻找某一特定的数据项
- 3.如何删除某一特定的数据项
- 4.如何迭代的访问各个数据项,以便进行显示或其他操作
而我们知道了数组的简单用法,现在用类的思想封装一个数组,实现上面的四个基本功能
ps:假设不会添加重复元素,这里没有考虑重复元素
public class testArray {
/**
* 定义一个数组
*/
private int[] intArray;
/**
* 定义数组的实际有效长度
*/
private int elems;
/**
* 定义数组的最大长度
*/
private int length;
/**
* 无参构造,构造一个长度为50的数组
*/
public testArray(){
elems= 0;
length = 50;
intArray = new int[length];
}
/**
* 构造函数,初始化一个长度为length的数组
* @param length
*/
public testArray(int length){
elems = 0;
this.length = length;
intArray = new int[length];
}
/**
* 获取数组的有效长度
* @return
*/
public int getSize(){
return elems;
}
/**
* 遍历显示元素
*/
public void display(){
for (int i = 0; i < elems ; i++) {
System.out.print(intArray[i]+" ");
}
System.out.println();
}
/**
* 添加元素
* @param 这里假设不会添加重复元素
* @return 添加成功返回true,添加的元素超过范围返回false
*/
public boolean add(int value){
if (elems == length){
return false;
}else {
intArray[elems] = value;
elems++;
}
return true;
}
/**
* 根据索引获取元素
* @param i
* @return 查找索引值在数组索引有效范围内,返回索引所表示的元素
* 查找索引超出数组索引有效值,提示索引越界
*/
public int get(int i){
if (i<0 || i>elems){
System.out.println("索引越界了");
}
return intArray[i];
}
/**
* 根据元素返回索引值
* @param searchValue
* @return 查找的元素如果存在则返回索引值,如果不存在,返回-1
*/
public int find(int searchValue){
int i ;
for (i = 0; i < elems ; i++){
if(intArray[i] == searchValue){
break;
}
}
if(i == elems){
return -1;
}
return i;
}
/**
* 删除元素
* @param value
* @return 如果要删除的值不存在,直接返回false,否则返回true,删除成功
*/
public boolean delete(int value){
int k = find(value);
if(k == -1){
return false;
}else {
if(k == elems-1){
elems --;
}else {
for (int i = k; i < elems -1 ; i++){
intArray[i] = intArray[i+1];
}
elems--;
}
return true;
}
}
/**
* 修改数据
* @param oldValue 原值
* @param newValue 新值
* @return 修改成功返回true,失败返回false
*/
public boolean modify(int oldValue,int newValue){
int i = find(oldValue);
if ( i == -1) {
System.out.println("需要修改的数据不存在");
return false;
}else {
intArray[i] = newValue;
return true;
}
}
}
测试类:
public class myArrayTest {
public static void main(String[] args) {
//创建自定义封装数组结构,数组大小为4
testArray array = new testArray(4);
//添加4个元素分别是1,2,3,4
array.add(1);
array.add(2);
array.add(3);
array.add(4);
//显示数组元素
array.display();
//查找索引为0的元素
int i = array.get(0);
System.out.println(i);
//删除值为4的元素
array.delete(4);
//将元素值3改为33
array.modify(3,33);
array.display();
}
}
结果为:
3.3、分析数组的局限性
通过上面的代码,我们发现数组是能够完成一个数据结构索引的功能的,而且实现起来也不难,那数组既然能完成所有的工作,我们实际应用中为啥不用它来进行所有的数据存储呢?
数组的局限性分析:
1、插入快。对于无序数组,上面实现的数组就是无序的,即元素没有按照从小到大或者某个特定的顺序排列,只是按照插入的顺序排列。无序数组增加一个元素很简单,只需要在数组末尾添加元素即可,但是有序数组就不一样了,它需要在指定的位置插入。
2、查找慢。当然如果根据索引来查找是很快的,但是我们通常都是根据元素值来查找的,给定一个元素值,对于无序数组,我们需要从数组第一个元素开始遍历,直到找到那个元素。有序数组通过特定的算法查找的速度会比无序数组快。
3、删除慢。根据元素值删除,我们要先找到该元素所处的位置,然后将元素后面的值整体向前面移动一个位置。也需要比较多的时间
4、数组一旦创建后,大小就固定了,不能动态扩展数组的元素个数。如果初始化一个很大的数组大小,那会白白浪费内存空间,如果给小了,后面数据个数增加了又添加不进去
很显然,数组虽然插入快,但是查找和删除都比较慢,而且扩展性差,所以我们一般不会用数组来存储数据,那有没有什么数据结构插入,查找,删除都很快,而且还能动态扩展存储个数大小呢,答案是有的,但是这是建立在很复杂的算法基础上。
4、数组排序
1、冒泡排序
这个名词的由来很好理解,一般水中的冒泡,水底刚冒出来的时候是比较小的,随着慢慢向水面浮起会逐渐增大。
冒泡排序的运作规律如下:
1、比较相邻的元素。如果第一个比第二个大,就交换二者
2、对每一对相邻元素作相同的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
3、针对所有的元素重复以上的步骤,除了最后一个
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
代码如下:
public class BubbleSort {
public static int[] sort(int[] array){
//这里for循环表示总共需要比较多少轮
for (int i = 1; i < array.length; i++){
//设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成
boolean flag = true;
//这里for循环表示每轮比较参与索引
//对当前无序区间array[0,......length-1]进行排序
//j的范围很关键,这个范围是在逐步缩小的,因为每轮比较都会将最大的放在右边
for (int j = 0; j < array.length-1 ; j ++){
if(array[j]>array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = false;
}
}
if(flag){
break;
}
//第i轮排序的结果:
System.out.print("第"+i+"轮排序的结果为");
display(array);
}
return array;
}
//遍历显示数组
public static void display(int[] array){
for (int i = 0 ; i < array.length ; i++){
System.out.print(array[i]+" ");
}
System.out.println();
}
public static void main(String[] args) {
int[] array = {4,2,8,9,5,7,6,1,3};
System.out.println("未排序数组顺序为:");
display(array);
System.out.println("-----------------------------");
array = sort(array);
System.out.println("冒泡排序后的数组顺序为:");
display(array);
}
}
结果如下:
本来应该是8轮排序的,这里我们只进行了7轮排序,因为第7轮之后就是有序数组了
解释:
冒泡排序是由两个for循环构成,第一个for循环的变量i表示总共需要多少轮比较,第二个for循环的变量j表示参与比较的元素索引【0,1,…,length-1】,因为每轮比较都会出现一个最大值放在最右边,所以每轮比较后的元素个数都会少一个,这也是为什么j范围是逐渐缩小的
分析:
假设参与比较的数组元素个数为N,则第一轮排序有N-1次比较,第二轮有N-2次,以此类推,这种序列的求和公式为:
(N-1)+(n-2)+…+1=N*(N-1)/2
当N的值很大时,算法比较次数约为N2/2次比较,忽略减1
假设数据是随机的,那么每次比较可能要交换位置,可能不会交换,假设概率为50%,那么交换次数为N2/4。不过如果是最坏的情况,那么每次比较都要交换位置
交换和比较次数都和N2成正比,由于常数不算大O表示法中,忽略2和4,那么冒泡排序运行都需要(N2)时间级别
其实无论何时,只要看见一个循环嵌套在另一个循环中,我们都可以怀疑这个算法的运行时间为O(N2)级,外层循环执行N次,内存循环对每一次外层循环都执行N次(或者几分之N次),这就意味着大约需要执行N2次基本操作
2、选择排序
选择排序是每一次从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
分为三步:
1、从带排序序列中,找到最小的元素
2、如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换
3、从余下的N-1个元素中,找出最小的元素,重复前面步骤,直到排序结束
代码如下:
public class ChoiceSort {
public static int[] sort(int[] array){
//总共要经过N-1轮比较
for (int i = 0; i < array.length ; i ++){
int min = i;
//每轮需要比较的次数
for (int j = i+1; j <array.length ; j++){
if(array[j]<array[min]){
min = j; //记录每轮最小值元素的索引
}
}
//将找到的最小值和i位置所在的值进行交换
if(i!=min){
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
System.out.println("第"+(i+1)+"轮排序后的结果为:");
display(array);
}
return array;
}
//遍历显示数组
public static void display(int[] array){
for(int i = 0; i < array.length; i ++){
System.out.print(array[i]+" ");
}
System.out.println();
}
public static void main(String[] args) {
int[] array = {4,2,8,9,5,7,6,1,3};
System.out.println("未排序数组为:");
display(array);
System.out.println("------------------------------");
array = sort(array);
System.out.println("------------------------------");
System.out.println("选择排序后的数组为:");
display(array);
}
}
运行结果:
性能分析:
选择排序和冒泡排序执行了相同次数的比较:N*(N-1)/2,但是至多只进行了N次交换
当N值很大时,比较次数是主要的,所以和冒泡排序一样,用大O表示是O(N2)时间级别。但是由于选择排序交换的次数少,所以选择排序无疑是比冒泡排序快的。当N值较小时,如果交换时间比选择时间大得多,那么选择排序是相当快的
3、插入排序
插入排序基本思想就是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所以元素为止。
插入排序还分为直接插入排序,二分插入排序,链表插入排序,希尔排序等待,这里只是以直接插入排序讲解
代码如下:
public class InsertSort {
public static int[] sort(int[] array){
int j;
//从索引为1的元素开始选择合适的位置插入,因为索引为0的只有一个元素,默认是有序的
for (int i = 1; i < array.length ; i++){
int temp = array[i]; //记录要插入的数据
j = i;
while(j>0 && temp < array[j-1]){ //从已经排序的序列最右边的开始比较,找到比其小的
array[j] = array[j-1]; //向后挪动
j--;
}
array[j] = temp; //存在比其小的数,插入
}
return array;
}
//遍历显示数组
public static void display(int[] array){
for(int i = 0; i < array.length; i ++){
System.out.print(array[i]+" ");
}
System.out.println();
}
public static void main(String[] args) {
int[] array = {4,2,8,9,5,7,6,1,3};
System.out.println("未排序数组为:");
display(array);
System.out.println("------------------------------");
array = sort(array);
System.out.println("------------------------------");
System.out.println("插入排序后的数组为:");
display(array);
}
}
运行结果:
性能分析:
在第一轮排序中,它最多比较一次,第二轮最多比较两次,以此类推,第N轮,最多比较N-1次,以此有1+2+3+…+N-1=N*(N-1)/2
假设在每一轮排序发现插入点时,平均只有全体数据项的一半进行了比较,除以2得到N*(N-1)/4.用大O表示法大致需要O(N2)时间级别
复制的次数大致等于比较的次数,但是一次复制与一次交换的时间耗时不同,所以相对于随机数据,插入排序比冒泡快一倍,比选择排序略快。如果要进行逆序排列,那么每次比较和移动都会进行,这时候并不会比冒泡排序快
冒泡,选择,插入用大O表示法都需要O(N2)时间级别,一般不会选择冒泡排序,虽然冒泡排序书写是最简单的,但是平均性能是没有选择排序和插入排序好的。
选择排序把交换次数降低到最低,但是比较次数还是挺大的,当数据量小,并且交换数据相当于比较数据更加耗时的情况下,可以应用选择排序
在大多数情况下,假设数据量比较小或者基本有序时,插入排序是三种算法中最好的选择。