一、涉及的整体内容大纲
二、具体内容
1.线性表
特点:
在数据元素的有限集中,除第一个元素无直接前驱,最后一个元素无直接后续以外,每个数据元素有且仅有一个直接前驱元素和一个直接后续元素。数组就是线性表的一种。
2.数组
2.1 数组基础操作
创建数组
在Java中创建数组,常用的方式为:
int[] intArray;
intArray = new int[100];
或者使用等价的方式
int[] intArray = new int[100];
但不建议用这种方式:int intArray[];
查看数组的大小:int arrayLength = intArray.length;
访问数组的数据项
一般通过数组的下标来访问,下标从0开始,注意数组下标越界的问题
int temp = intArray[1];
intArray[2] = 66;
初始化
创建数组后,如果不另行指定,整数型的数组会自动初始化成0。即便通过方法来定义数组也是如此。 如果创建一个对象数组,如autoData[] carArray = new autoData[400]; 那么自动初始化为null对象。当尝试访问其中的数据项时,会出现Null Pointer Assignment 空指针赋值的错误。
需要初始化时:
int[] intArray = {1,2,3,4,5};
下面利用代码实现基础的数组增删改查
public class MyArrays {
public static void main(String[] args) {
//创建数组
long[] arr;
arr = new long[10];
//long arr = new long[10];这么写也是可以的
//要查找的数
long searchKey;
//向数组中插入值
arr[0] = 88;
arr[1] = 22;
arr[2] = 11;
arr[3] = 55;
//遍历数组中的元素,并且打印
for (int i = 0; i < arr.length; i++) {
//打印数组中的值
System.out.print(arr[i] + " ");
}
//查找数组中的某个值
searchKey = 22;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == searchKey) {
System.out.println("找到要找的值" + searchKey);
}else if(i == arr.length) {
//找不到改值
System.out.println("找不到该值");
}
}
//查找的另外一种做法
searchKey = 23;
//讲循环的控制变量调用出来
int x;
for (x = 0; x < arr.length; x++) {
if (arr[x] == searchKey) {
break;
}
}
if (x == arr.length) {
System.out.println("找不到该值");
}else {
System.out.println("找到要找的值" + searchKey);
}
//删除某一个值
searchKey = 22;
for (int i = 0; i < arr.length; i++) {
//找到该值后
if (arr[i] == searchKey ) {
//将其后面的值都向前移位
for (int j = i; j < arr.length - 1; j++) {
//这里的循环次数减一是防止数组越界
arr[j] = arr[j + 1];
}
}
}
//删除的另一种做法
searchKey = 33;
for (x = 0; x < arr.length; x++) {
if(arr[x] == searchKey) {
break;
}
}
//好处是避免了找到该值后还继续查找
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i + 1];
}
//再次遍历该数组,打印值
for (int i = 0; i < arr.length; i++) {
//打印数组中的值
System.out.print(arr[i] + " ");
}
}
}
/*
* 运行结果:
* 88 22 11 55 0 0 0 0 0 0 找到要找的值22
88 11 55 0 0 0 0 0 0 0
*/
但是也可以利用面向对象的思想解决
public class LowArray {
//创建数组
private long[] arr;
//创建数组的大小值
private int size;
//带参构造初始化数组
public LowArray(int size) {
arr = new long[size];
this.size = 0;
}
/**
* 数组的插入
* @param value
*/
public void insert(long value) {
arr[size] = value;
size++;
}
/**
* 获取数组中的值
* @param index
* @return
*/
public long getElement(int index) {
return arr[index];
}
/**
* 查找数组中的值
* @param searchKey
* @return
*/
public boolean find(long searchKey) {
boolean flag = false;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == searchKey) {
//找到该值就可以改为true
flag = true;
}
}
return flag;
}
/**
* 实现数组的删除
* @param value
* @return
*/
public boolean delete(long searchKey) {
boolean flag = false;
//该值存在才可以进行删除
if (this.find(searchKey)) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == searchKey) {
//将数组后面的值前移
for(int j = i; j < arr.length - 1; j++) {
arr[j] = arr[j+1];
}
flag = true;
size--;
break;
}
}
}
return flag;
}
/**
* 打印数组的值
*/
public void display() {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
查找方法
通过上述的代码可以看出来,简单的线性查找就是:遍历一遍数组中的值,依次比较数组中的值和要比较的值,最后得出来结果。
当然对于数据很多的数组来说,这种查找方式就会很慢。所以下面介绍二分查找的方式,这种方式的前提是针对有序数组,数组的排序方式必须是从小到大的形式。
二分查找方式思想的简述:对于一个数组,从小到大的方式排列后,从中间一分为二,小于中间的为A段,另外的就是B段,取出数组中间的值,如果要查找的值比中间的值大,则该值就在中间值的后面,在B段。那么就可以将B段重新视为一个数组,再取出B段的中间值进行比较。在A段就是同样的道理。
代码如下:
/**
* 实现二分查找返回数据所在的位置
* @param searchKey
* @return
*/
public int binaryFind(long searchKey) {
//设置二分查找的上界
int lowerBound = 0;
//设置二分查找的上界
int upperBound = size - 1;
//二分查找的中间位置
int curIn;
while(true) {
//因为需要不断变化中间位置,放到中间
curIn = (lowerBound + upperBound) / 2;
//判断中间位置
if (arr[curIn] == searchKey) {
return curIn;
}else if(lowerBound > upperBound) {
//如果查找结束
return size;
}else {
if (arr[curIn] < searchKey) {
//要找的值大于中间值,更改下界
lowerBound = curIn + 1;
}else {
//反之,更改上界
upperBound = curIn - 1;
}
}
}//结束while循环
}//方法结束
这个查找的方法需要注意的是在查找到最后数组上界小于下界的问题。
有序数组
从上面的查找方式来看,有序数组的查找速度比无序数组快多了。但是在插入时都需要将靠后的数据移动来腾出空间,速度较慢。有序数组和无序的数组删除操作都很慢,因为数据项必须向前移动来填补已删除数据项的洞(防止在查找的时候出现数据项为空的现象)。所以有序数组适合在查找频繁的情况下使用,但是如果插入与删除比较频繁时,则无法高效工作。
为什么说二分法查找的快
首先需要明白的是查找的速度快慢是根据在一定的范围内,查找一个数最多要几步。对于线性查找,10条的记录需要比较5次(n/2),而对于二分查找最多需要四次,100条线性查找需要50次,二分查找需要7次。而计算二分查找的次数是根据2的幂次来计算。
从图中可以看到,范围r中,到100条的范围也就是128是,2的7次方,二次查找就是求它的对数,结果就是7次。同理对于10,就是4次。
而在讲究算法的速度时,不免就需要谈到大O表示法。
2.2 大O表示法
定义:描述算法的速度是如何与数据项的个数相联系的比较。下面介绍一些常见算法的速度。
无序数组的插入:常数
这是唯一一个与数组中数据项的个数无关的算法,新的数组项总是被放在下一个有空的地方。所以无论数组中数据项N有多大,一次插入总是用相同的时间。所以我们可以说向一个无序数组中插入一个数据项的时间是一个常数K。
Time = K
但是在现实中插入的运行速度还与其他因素有关,如处理器的计算速度,编译程序生成代码的效率等,但都被包括在K常数中。
线性查找:与N成正比
一般地,寻找特定数据项所需的比较次数平均为数据项总数的一半。
T = K * N / 2
对与该方程中的K值,首先需要对某个N值的查找进行计时,然后用T计算出K,之后就可以对任意N的值进行计算。
如果把2并入K中的话,就可以得到一个更简单的公式:
T = K * N
所以通过该公式可以看出来,平均线性查找的时间与数组的大小成正比。
二分查找:与log(N)成正比
根据上面对二分查找速度的描述,可以得出来:
T = K * log2(N)
同时因为所有的对数和其他的对数成比例(从底数为2转换到底数为10需乘上3.322),所以我们亦可以将常数的底数并入K,不必指定指数。
T= K * log(N)
但是在大O表示法中,直接省去了常数K,比较算法的时候,并不在乎具体的影响因素;真正需要比较的是对应不同的N值,T是如何变化的,而不是具体的数字,因此不需要常数。
总结上面讨论的算法运行时间
下面是运行的时间图比较
2.3 数组的缺陷
第一点就是不同类型的数组实现的特点不同,有序数组插入块,查找慢,有序数组查找快,插入慢,两者的删除都慢。
第二点就是在数组创建之后,大小尺寸就会被固定住了。如果数组设置过大,就是浪费空间,过小就会数组溢出。
2.4 简单排序
排序的基本思想:
1.比较两个数据项,
2.交换两个数据项,或者复制其中的一项
3.循环执行以上两步,直到全部的数据有序为止。
冒泡排序
遵循的规则:
1.比较数组中的两个数据
2.如果前一个数大,则两个数交换位置
3.向右移动一个位置,比较下面两个数据。
最后最大的数就会在最右边,下一次比较的时候,最后的位置就不需要进行比较了。
实现代码:
/**
* 冒泡排序先从后面进行排序
* @param maxSize
*/
public void bubbleSortFromLast() {
int out, in;
//从最后面取值进行排序
for(out = arr.length - 1; out > 1; out--) {
for (in = 0; in < out; in++) {
if (arr[in] > arr[in + 1]) {
swap(in, in + 1);
}
}
}
}
/**
* 冒泡排序从前面进行排序
* @param maxSize
*/
public void bubbleSortFromFirst() {
for(int out = 0; out < arr.length - 1; out++) {
for (int in = 0; in < arr.length - 1 - out; in++) {
if (arr[in] > arr[in + 1]) {
swap(in, in + 1);
}
}
}
}
/**
* 交换数组中的两个值
* @param in
* @param out
*/
private void swap(int in, int out) {
long temp = arr[in];
arr[in] = arr[out];
arr[out] = temp;
}
从上面的代码可以看出来,
第一种冒泡排序的外层循环从数组末尾开始,每次循环后排序好的就是最右边out个数据项,所以out--就只对未排序的数据项进行排序。
内层循环从数组头开始,遇到更大的数就会往后交换,所以最后比较的数都是每次内层循环中最大的数,实现冒泡的思想。
因为不需要对最后排序好的最大的数进行比较,所以内层循环小于out。
第二种冒泡排序的思想是外层循环从数组开头开始,同样每次内层循环结束后,最大的数就会在最后面,但排序好的不需要重复,所以内层循环需要减掉out,内外层循环都-1是为了in+1的时候数组不会越界报错。
冒泡排序的效率
从每次循环的次数来看,总共的计算次数为(N - 1)+(N - 2)+(N - 3)+ ... + 3 + 2 + 1 = N * (N - 1) / 2
如果用大O表示法表示的话,忽略一般的常数项,则为O(N2)(N的二次方)
选择排序
遵循的规则
1.先对所有的数据项进行比较,将最小的数挑出来,然后和数组的第一个数进行交换
2.在将除了第一个数的其他数组项拿出来,把最小的数挑出来,然后和数组的第二个数进行交换
3.按照步骤二接下去循环,直到所有的值都已经排序好
注意的要点:
1.选择排序的核心在于区分已经排序好的数据项和为排序好的数据项
2.选择排序的有序数据项在左边,冒牌排序的在右边
代码如下:
/**
* 选择排序
*/
public void selectSort() {
int out, in, min;
for (out = 0; out < arr.length - 1; out++) {
min = out;
for (in = out + 1; in < arr.length; in++) {
if (arr[in] < arr[min]) {
min = in;
swap(out, min);
}
}
}
}
从上面的代码可以看出来,外层循环从数组头开始,因为只需要从提取第二个数开始和第一个数比较,所以内层循环为out+1开始,只要和第一个数比较就可以选择最小的数出来。同时,min的作用为区分有序数列和无序数列。
选择排序的效率
根据代码的写法来看,选择排序和冒泡排序的循环次数是相同的,也就是需要进行N * (N - 1) / 2次比较,但因为交换的时候,只是交换每次循环最小的值,所以对于选择排序交换的次数最多就是数组的大小(假设数据是从大到小排列,再换成从小到大)。所以总的来看,选择排序和冒泡排序的比较次数相同,但是交换次数远小于冒泡排序,当交换数据花的时间比比较大小花的时间要多时,选择排序比冒泡排序要快很多。
插入排序
遵循的规则
1.插入排序需要注意的是在数组中要分成已经有序的数据项和还未排序的数据项,也就是局部有序。一般在插入排序中左边的数据项是已经排序好的,右边的未排序好的
2.而介于排序和未排序之间的数据,被称为标记数据,它和它的右边都是未排序的数据项
3.接着就让标记数据和它前一个数据比大小,如果前一个数据大,将大的数据往后移动,腾出位置再让标记数据和前一个数据进行比较,如果比它大,再后移腾出空位,直到没有比它小的数,然后插入到腾出的空位
4.这样有序数列就增多,无序数列就减少。一直循环到无序数列为0为止
代码如下:
/**
* 插入排序
*/
public void insertionSort() {
int in, out;
//设置外层循环,则为排序的部分
for (out = 1; out < arr.length; out++) {
//把每一个值都拿出来
long temp = arr[out];
//将内层循环设置与外层循环同步
in = out;
//只有在
while(in > 0 && arr[in - 1] >= temp) {
arr[in] = arr[in - 1];
--in;
}
arr[in] = temp;
}
}
从上面的代码中,
外层循环从下标1开始,因为数组第一个数默认为有序数列,所以out就是作为标记数据的存在,先将它保存到temp中,外层循环遍历的是无序数列。
再将内层的循环开始设置为标记数据,也就是有序数列末尾的后一个数,对有序数列中的数进行遍历,只有在每次循环后有序数列中还有数据项并且标记数据的前一个数大于或者等于标记数据时,才可以进入循环体,否则直接作为有序数列的末尾就可以(也就是有序数列中最大的值)。
进入循环体中,将大的数进行后移,腾出来空位,直到有序数列中没有数比标记数据大,在将标记数据插入到空位中,之后回到外层进行下一次循环。
插入排序的效率
首先计算该算法的比较次数,第一次排序中,最多比较一次(只有两个数),第二次最多两次,以此类推比较的次数为
1 + 2 + 3 + ... + (N - 1)= N * (N - 1) / 2。
但是每次插入数据之前,平均来看全部的数据项中只有一半进行了比较,所以真正的比较次数为
N * (N - 1) / 4
而复制的次数与比较的次数差不多,综上,插入排序的算法比冒泡排序快一倍,比快速排序只是略快。但是在大O表示法下,都是同样的级别O(N2)(N的二次方),但是对于已经有序的数组来说,插入排序的方式在While循环时不会进入循环体,因为内部的数据已经排序好了,所以只会执行外层排序,总共就是N - 1次比较,需要的时间就是O(N)级别,远小于另外两种排序方式。
对象排序
对象排序的功能还是通过先对对象的成员变量的具体内容进行比较,然后在进行排序。
代码如下:
/**
* 对对象数组进行插入排序
*/
public void insertionSortForObject() {
int in, out;
//设置外层循环,则为排序的部分
for (out = 1; out < arr.length; out++) {
//把每一个值都拿出来
long temp = arr[out];
//将内层循环设置与外层循环同步
in = out;
//只有在
while(in > 0 && arr[in - 1].getLast().compareTo(temp.getLast()) > 0) {
arr[in] = arr[in - 1];
--in;
}
arr[in] = temp;
}
}
其中getLast()方法是该对象的成员方法用于提取字符串last,然后利用String类中的compareTo()方法进行比较。
通过字符串的比较顺序对数据项进行排序
简单排序总结
冒泡排序太简单,而且查询的效率很低,一般不会使用。
选择排序因为比较的次数依旧很大,所以只有在数据量很小的时候,并且交换数据比比较数据更加耗时的情况下,可以选择排序。
当数据量比较小或者基本上有序的情况下,可以使用插入排序。