数据结构之数组
数组是应用最广泛的数据存储结构。它被植入到大部分编程语言中。由于数组十分易懂,所以它被用来作为介绍数据结构的起步点,并展示面向对象编程和数据结构之间的相互关系。
一、数组的基础知识
1.创建数组
Java中有两种数据类型:基本类型(如int和double)和对象类型。在许多编程语言中(甚至有些面向对象语言,如C++),数组也是基本类型,但在Java中把它们当作对象来对待,因此在创建数组时必须使用new操作符:
数组有一个length字段,通过它可以得知当前数组大小(数据项的个数):
数组数据项通过使用方括号中的下标数来访问。这与其他语言类似:
3.初始化
当创建整型数组之后,如果不另行指定,那么整型数组会自动初始化为空。与C++不同的是,即使通过方法(函数)来定义数组也是这样的。创建一个对象数组如下:
删除数据项55之前的数组:66 99 44 55 11 0 33 88 22 77
查找数据项66的结果:Found 66
删除数据项55之后的数组:66 99 44 11 0 33 88 22 77
ArrayApp.java的结构中有些部分还是需要改进的。程序中只有一个类ArrayApp,这个类只有一个方法main()。ArrayApp.java实际上是一个老式的面向过程的程序。使之更加对象化可以让程序更加易懂。
改进ArrayApp.java的结构分两个步骤进行:
首先将数据存储结构(数组arr)从程序中分离出来,这样做的好处是使程序中的其他部分成为使用这个结构的用户。
接着改进存储结构和用户之间的通信。
删除数据项(22、55和77)之前的数组:66 99 44 55 11 0 33 88 22 77
查找数据项35的结果:Can't find 35
删除数据项(22、55和77)之后的数组:66 99 44 11 0 33 88
二、有序数组
假设一个数组,其中的数据项按关键字升序排列,即最小值在下标为0的单元上,每一个单元都比前一个单元的值大。这种数组被称为有序数组。
当向这种数组中插入数据项时,需要为插入操作找到正确的位置:刚好在稍小值的后面,稍大值的前面。然后将所有比待插数据项大的值向后移以便腾出空间。这样做的好处是预先设定了不允许重复,提高了查找的速度,但降低了插入操作的速度。
1.线性查找
有序数组可以选择两种查找算法:线性查找和二分查找。缺省情况下是线性查找。从有序数组的一端开始,依次将每个数据项的关键字与查找关键字进行匹配,若有想等者,则查找成功;否则继续匹配,直到匹配完所有数据项,仍未有相等者,则查找不成功,给出结果信息。主要用于数据项的个数较小的数组。
2.二分查找
从有序数组的中间位置开始匹配,如果当前数据项的关键字等于查找关键字,则查找成功;否则,若查找关键字小于当前数据项的关键字,则在有序数组的前半段继续查找;反之,在后半段继续查找,以此重复,直到获得查找结果(成功或不成功)。
3.有序数组的Java代码
删除数据项(22、55和77)之前的数组:0 11 22 33 44 55 66 77 88 99
删除数据项(22、55和77)之后的数组:0 11 33 44 66 88 99
三、大O表示法
汽车按尺寸被分为若干类:微型、小型、中型等等。在不提及具体尺寸的情况下,这些分类可以为我们所涉及到车的大小提供一个大致概念。我们同样也需要一种快捷的方法来评价计算机算法的效率。在计算机科学中,这种粗略的度量方法被称为“大O”表示法。
1.无序数组的插入:常数
无序数组的插入,新数据项总是被放在下一个有空的地方,arr[nElems],然后nElems增加。无论数组中的数据项个数N有多大,一次插入总是花费相同的时间。我们可以说向一个无序数组中插入一个数据项的时间T是一个常数K:
T = K
在现实情况中,插入所需的实际时间(不管是微秒还是其他单位)与以下这些因素有关:微处理器、编译程序生成程序代码的效率,等等。上面等式中的常数K包含了所有这些元素。在现实情况中要得到K的值,需要测量一次插入所花费的时间。K就等于这个时间。
2.线性查找:与N成正比
在数组数据项的线性查找中,我们已经发现寻找特定数据项所需的比较次数平均为数据项总数的一半。因此设N为数据项的总个数,搜索时间T与N的一半成正比:
T = K * N/2
同插入一样,若要得到方程中K的值,首先需要对某个N值(有可能很大)的查找进行计时,然后用T来计算K。当得到K后便可对任意N的值来计算T。
将2并入K可以得到一个更方便的公式。新K值等于原先的K除以2。新公式为:
T = K * N
这个方程说明平均线性查找时间与数组的大小成正比。即如果一个数组增大两倍,则所花费的查找时间也会相应地增长两倍。
3.二分查找:与log(N)成正比
同样,我们可以为二分查找制定一个与T和N有关的公式:
T = K * log2(N)
正如前面所提到的,时间T与以2为底N的对数成正比。实际上,由于所有的对数都和其他对数成比例(从底数为2转换到底数为10需乘以3.322),我们可以讲这个为常数的底数也并入K。由此不必指定底数:
T = K * log(N)
4.不要常数
大O表示法同上面的公式比较类似,但它省去了常数K。当比较算法时,并不在乎具体的微处理器芯片或编辑器;真正需要比较的是对应不同的N值,T是如何变化的。而不是具体的数字。因此不需要常数。
大O表示法使用大写字母O,可以认为其含义是“order of”(大约是)。我们可以使用大O表示法来描述线性查找使用了O(N)级时间,二分查找使用了O(logN)级时间。向一个无序数组中的插入使用了O(1),或常数级时间。
四、数组局限性
为什么不用数组来进行所有的数据存储呢?我们已经见到了许多关于数组的缺点。在一个无序数组中可以很快进行插入(O(1)时间),但是查找却要花费较慢的O(N)时间。在一个有序数组中可以查找得很快,花费(O(logN))的时间,但插入缺花费了O(N)时间。对于这两种数组而言,由于平均半数的数据项为了填补“空洞”必须移动,所以删除操作平均需要O(N)时间。
数组的另外一个问题便是它们被new创建后,大小尺寸就被固定了。但通常在开始设计程序时并不知道会有多少数据项将会放入数组中,所以需要猜它的大小。如果猜的数过大,会使数组中的某些单元永远不会被填充而浪费空间。如果猜得过小,会发生数组的溢出,最好的情况下会向程序的用户发出警告信息,最好的情况则会导致程序崩溃。
PS:Java中有一个被称为Vector的类,使用起来很像数组,但是它可以扩展。当类用户使用类中的内部数组将要溢出时,插入算法创建一个大一点的数组,把旧数组中的内容复制到新数组中,然后再插入新数据项。整个过程对于类用户来说是不可见的。但这些附加的功能是以效率作为代价的。
一、数组的基础知识
1.创建数组
Java中有两种数据类型:基本类型(如int和double)和对象类型。在许多编程语言中(甚至有些面向对象语言,如C++),数组也是基本类型,但在Java中把它们当作对象来对待,因此在创建数组时必须使用new操作符:
int[] intArray; // defines a reference to an array intArray = new int[100]; // creates the array, and sets intArray to refer to it
或使用等价的单语句声明的方法:
int[] intArray = new int[100];
[]操作符对于编译器来说是一个标志,它说明正在命名的是数组对象而不是普通的变量。当然还可以通过另一种语法来使用这个操作符,将它放在变量名的后面,而不是类型后面:
int intArray[] = new int[100]; // alternative syntax
但是将[]放在int后面会清楚地说明[]是数据类型的一部分,而不是变量名的一部分。
由于数组是一个对象,所以它的名字(前面程序中的intArray)是数组的一个引用:它不是数组本身。数组存储在内存中的其他地址中,而intArray仅仅保存着这个地址。数组有一个length字段,通过它可以得知当前数组大小(数据项的个数):
int arrayLength = intArray.length; // find array size
正如大多数编程语言一样,一旦创建数组,数组大小便不可改变。
2.访问数组数据项数组数据项通过使用方括号中的下标数来访问。这与其他语言类似:
temp = intArray[3]; // get contents of fourth element of array intArray[7] = 66; // insert 66 into the eighth cell
请注意无论是在C、C++,还是Java中,第一个数据项的下标都是0,所以一个有10个数据项的数组下标是从0至9。
如果访问小于0或比数组大小大的数据项,程序会出现Array Index Out of Bounds(数组小标越界)的运行时错误。3.初始化
当创建整型数组之后,如果不另行指定,那么整型数组会自动初始化为空。与C++不同的是,即使通过方法(函数)来定义数组也是这样的。创建一个对象数组如下:
autoData[] carArray = new autoData[4000];
除非将特定的值赋给数组的数据项,否则它们一直是特殊的null对象。如果尝试访问一个含有null的数组数据项,程序会出现Null Pointer Assignment(空指针赋值)的运行时错误。这主要是为了保证在读取某个数据项之前要先对其赋值。
使用下面的语法可以对一个基本类型的数组初始化,赋入非空值:int[] intArray = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
上面的语句可能简单得令人惊讶,它同时取代了引用声明和使用new来创建数组。在大括号中的数据被称为初始化列表。数组大小由列表数据项的个数决定。
4.数组的Java代码ArrayApp.java
package com.leverage.array; public class ArrayApp { public static void main(String[] args) { long[] arr; // reference to array arr = new long[100]; // make array int nElems = 0; // number of items int j; // loop counter long searchKey; // key of item to search for // --------------------------------------------------------------- arr[0] = 66; // insert 10 items arr[1] = 99; arr[2] = 44; arr[3] = 55; arr[4] = 11; arr[5] = 00; arr[6] = 33; arr[7] = 88; arr[8] = 22; arr[9] = 77; nElems = 10; // new 10 items in array // --------------------------------------------------------------- for (j = 0; j < nElems; j++) { // display items System.out.print(arr[j] + " "); } System.out.println(""); // --------------------------------------------------------------- searchKey = 66; // find item with key 66 for (j = 0; j < nElems; j++) { // for each element if (arr[j] == searchKey) { // found item? break; // yes, exit before end } } if (j == nElems) { // at the end? System.out.println("Can't find " + searchKey); // yes } else { System.out.println("Found " + searchKey); // no } // --------------------------------------------------------------- searchKey = 55; // delete item with key 55 for (j = 0; j < nElems; j++) { // look for it if (arr[j] == searchKey) { break; } } for (int k = j; k < nElems; k++) { // move higher ones down arr[k] = arr[k + 1]; } nElems--; // decrement size // --------------------------------------------------------------- for (j = 0; j < nElems; j++) { // display items System.out.print(arr[j] + " "); } System.out.println(""); } }在这个程序中创建了一个名为arr的数组,并赋进10个数据,在所有数据项中查找66并在屏幕上显示所有的数据项,删除55,随后显示了剩下的9个数据项。程序的输出如下:
删除数据项55之前的数组:66 99 44 55 11 0 33 88 22 77
查找数据项66的结果:Found 66
删除数据项55之后的数组:66 99 44 11 0 33 88 22 77
ArrayApp.java的结构中有些部分还是需要改进的。程序中只有一个类ArrayApp,这个类只有一个方法main()。ArrayApp.java实际上是一个老式的面向过程的程序。使之更加对象化可以让程序更加易懂。
改进ArrayApp.java的结构分两个步骤进行:
首先将数据存储结构(数组arr)从程序中分离出来,这样做的好处是使程序中的其他部分成为使用这个结构的用户。
接着改进存储结构和用户之间的通信。
HighArray.java
package com.leverage.array; public class HighArray { private long[] arr; // refer to array arr private int nElems; // number of data items // ------------------------------------------------------------------- public HighArray(int max) { // constructor arr = new long[max]; // create the array nElems = 0; // no items yet } // ------------------------------------------------------------------- public boolean find(long searchKey) { // find specified value int j; for (j = 0; j < nElems; j++) { // for each element if (arr[j] == searchKey) { // found item? break; // exit loop before end } } if (j == nElems) { // gone to end? return false; // yes, can't find it } else { return true; // no, found it } } // ------------------------------------------------------------------- public void insert(long value) { // put element into array arr[nElems] = value; // insert it nElems++; // increment size } // ------------------------------------------------------------------- public boolean delete(long value) { int j; for (j = 0; j < nElems; j++) { // look for it if (value == arr[j]) { break; } } if (j == nElems) {// can't find it return false; } else { // found it for (int k = j; k < nElems; k++) { // move higher ones down arr[k] = arr[k + 1]; } nElems--; // decrement size return true; } } // ------------------------------------------------------------------- public void display() {// displays array contents for (int j = 0; j < nElems; j++) { System.out.print(arr[j] + " "); } System.out.println(""); } }
HighArrayApp.java
package com.leverage.array; public class HighArrayApp { public static void main(String[] args) { int maxSize = 100; // array size HighArray arr; // reference to array arr = new HighArray(maxSize); // create the array arr.insert(66); // insert 10 items arr.insert(99); arr.insert(44); arr.insert(55); arr.insert(11); arr.insert(00); arr.insert(33); arr.insert(88); arr.insert(22); arr.insert(77); arr.display(); // display items int searchKey = 35; // search for item if (arr.find(searchKey)) { System.out.println("Found " + searchKey); } else { System.out.println("Can't find " + searchKey); } arr.delete(22); // delete 3 items arr.delete(55); arr.delete(77); arr.display(); // display items again } }在改进后程序中,数组arr被封装在HighArray类中,HighArrayApp类的main()方法创建了一个数组,插入10个数据项,查找一个不存在的数据项35,显示数组的内容。由于删除操作十分容易,程序删除了三个数据项(22、55和77)而不是一个,最后再次显示数组的内容。程序的输出日下:
删除数据项(22、55和77)之前的数组:66 99 44 55 11 0 33 88 22 77
查找数据项35的结果:Can't find 35
删除数据项(22、55和77)之后的数组:66 99 44 11 0 33 88
二、有序数组
假设一个数组,其中的数据项按关键字升序排列,即最小值在下标为0的单元上,每一个单元都比前一个单元的值大。这种数组被称为有序数组。
当向这种数组中插入数据项时,需要为插入操作找到正确的位置:刚好在稍小值的后面,稍大值的前面。然后将所有比待插数据项大的值向后移以便腾出空间。这样做的好处是预先设定了不允许重复,提高了查找的速度,但降低了插入操作的速度。
1.线性查找
有序数组可以选择两种查找算法:线性查找和二分查找。缺省情况下是线性查找。从有序数组的一端开始,依次将每个数据项的关键字与查找关键字进行匹配,若有想等者,则查找成功;否则继续匹配,直到匹配完所有数据项,仍未有相等者,则查找不成功,给出结果信息。主要用于数据项的个数较小的数组。
2.二分查找
从有序数组的中间位置开始匹配,如果当前数据项的关键字等于查找关键字,则查找成功;否则,若查找关键字小于当前数据项的关键字,则在有序数组的前半段继续查找;反之,在后半段继续查找,以此重复,直到获得查找结果(成功或不成功)。
3.有序数组的Java代码
OrderedArray.java
package com.leverage.array; public class OrderedArray { private long[] arr; // refer to array arr private int nElems; // number of data items // ------------------------------------------------------------ public OrderedArray(int max) { // constructor arr = new long[max]; // create array nElems = 0; } // ------------------------------------------------------------ public int size() { return nElems; } // ------------------------------------------------------------ public int find(long searchKey) { int lowerBound = 0; int upperBound = nElems - 1; int curIn; while (true) { curIn = (lowerBound + upperBound) / 2; if (arr[curIn] == searchKey) { return curIn; // found it } else if (lowerBound > upperBound) { return nElems; // can't find it } else { // divide range if (arr[curIn] < searchKey) { lowerBound = curIn + 1; // it's in upper half } else { upperBound = curIn - 1; // it's in lower half } } } } // ------------------------------------------------------------ public void insert(long value) { // put element into array int j; for (j = 0; j < nElems; j++) { // find where it goes if (arr[j] > value) { // linear search break; } } for (int k = nElems; k > j; k--) { // move bigger ones up arr[k] = arr[k - 1]; } arr[j] = value; // insert it nElems++; } // ------------------------------------------------------------ public boolean delete(long value) { int j = find(value); if (j == nElems) { // can't find it return false; } else { // found it for (int k = j; k < nElems; k++) { // move bigger ones down arr[k] = arr[k + 1]; } nElems--; return true; } } // ------------------------------------------------------------ public void display() { // displays array contents for (int j = 0; j < nElems; j++) { // for each element System.out.print(arr[j] + " "); // display it } System.out.println(""); } }
OrderedArrayApp.java
package com.leverage.array; public class OrderedArrayApp { public static void main(String[] args) { int maxSize = 100; // array size OrderedArray arr; // reference to array arr = new OrderedArray(maxSize); // create the array arr.insert(66); // insert 10 items arr.insert(99); arr.insert(44); arr.insert(55); arr.insert(11); arr.insert(00); arr.insert(33); arr.insert(88); arr.insert(22); arr.insert(77); int searchKey = 55; // search for item if (arr.find(searchKey) != arr.size()) { System.out.println("Found " + searchKey); } else { System.out.println("Can't find " + searchKey); } arr.display(); // display items arr.delete(22); // delete 3 items arr.delete(55); arr.delete(77); arr.display(); // display items again } }查找数据项55的结果:Found 55
删除数据项(22、55和77)之前的数组:0 11 22 33 44 55 66 77 88 99
删除数据项(22、55和77)之后的数组:0 11 33 44 66 88 99
三、大O表示法
汽车按尺寸被分为若干类:微型、小型、中型等等。在不提及具体尺寸的情况下,这些分类可以为我们所涉及到车的大小提供一个大致概念。我们同样也需要一种快捷的方法来评价计算机算法的效率。在计算机科学中,这种粗略的度量方法被称为“大O”表示法。
1.无序数组的插入:常数
无序数组的插入,新数据项总是被放在下一个有空的地方,arr[nElems],然后nElems增加。无论数组中的数据项个数N有多大,一次插入总是花费相同的时间。我们可以说向一个无序数组中插入一个数据项的时间T是一个常数K:
T = K
在现实情况中,插入所需的实际时间(不管是微秒还是其他单位)与以下这些因素有关:微处理器、编译程序生成程序代码的效率,等等。上面等式中的常数K包含了所有这些元素。在现实情况中要得到K的值,需要测量一次插入所花费的时间。K就等于这个时间。
2.线性查找:与N成正比
在数组数据项的线性查找中,我们已经发现寻找特定数据项所需的比较次数平均为数据项总数的一半。因此设N为数据项的总个数,搜索时间T与N的一半成正比:
T = K * N/2
同插入一样,若要得到方程中K的值,首先需要对某个N值(有可能很大)的查找进行计时,然后用T来计算K。当得到K后便可对任意N的值来计算T。
将2并入K可以得到一个更方便的公式。新K值等于原先的K除以2。新公式为:
T = K * N
这个方程说明平均线性查找时间与数组的大小成正比。即如果一个数组增大两倍,则所花费的查找时间也会相应地增长两倍。
3.二分查找:与log(N)成正比
同样,我们可以为二分查找制定一个与T和N有关的公式:
T = K * log2(N)
正如前面所提到的,时间T与以2为底N的对数成正比。实际上,由于所有的对数都和其他对数成比例(从底数为2转换到底数为10需乘以3.322),我们可以讲这个为常数的底数也并入K。由此不必指定底数:
T = K * log(N)
4.不要常数
大O表示法同上面的公式比较类似,但它省去了常数K。当比较算法时,并不在乎具体的微处理器芯片或编辑器;真正需要比较的是对应不同的N值,T是如何变化的。而不是具体的数字。因此不需要常数。
大O表示法使用大写字母O,可以认为其含义是“order of”(大约是)。我们可以使用大O表示法来描述线性查找使用了O(N)级时间,二分查找使用了O(logN)级时间。向一个无序数组中的插入使用了O(1),或常数级时间。
四、数组局限性
为什么不用数组来进行所有的数据存储呢?我们已经见到了许多关于数组的缺点。在一个无序数组中可以很快进行插入(O(1)时间),但是查找却要花费较慢的O(N)时间。在一个有序数组中可以查找得很快,花费(O(logN))的时间,但插入缺花费了O(N)时间。对于这两种数组而言,由于平均半数的数据项为了填补“空洞”必须移动,所以删除操作平均需要O(N)时间。
数组的另外一个问题便是它们被new创建后,大小尺寸就被固定了。但通常在开始设计程序时并不知道会有多少数据项将会放入数组中,所以需要猜它的大小。如果猜的数过大,会使数组中的某些单元永远不会被填充而浪费空间。如果猜得过小,会发生数组的溢出,最好的情况下会向程序的用户发出警告信息,最好的情况则会导致程序崩溃。
PS:Java中有一个被称为Vector的类,使用起来很像数组,但是它可以扩展。当类用户使用类中的内部数组将要溢出时,插入算法创建一个大一点的数组,把旧数组中的内容复制到新数组中,然后再插入新数据项。整个过程对于类用户来说是不可见的。但这些附加的功能是以效率作为代价的。