1.概述
1.数据结构的优缺点
数据结构 优点 缺点 数组 插入快,如果知道下标,可以非常快地存取 查找慢,删除慢,大小固定 有序数组 比无序数组查找快 删除和插入慢,大小固定 栈 提供后进先出的存取 存取其他项很慢 队列 提供先进先出方式的存取 存取其他项很慢 链表 插入快,删除快 查找慢 二叉树 查找,插入,删除都快(如果树保持平衡) 删除算法复杂 红-黑树 查找,插入,删除都快,树总是平衡的 算符复杂 2-3-4树 查找,插入,删除都快,树总是平衡的。类似的树对磁盘存储有用 算法复杂 哈希表 如果关键字已知则存取极快;插入快 删除慢,如果不知道关键字则存取很慢,对存储空间使用不充分 堆 插入,删除快,对最大数据项的存取很快 对其他数据项存取慢 图 对现实世界建模 有些算法慢且复杂 - 上述数据结构除数组之外都可以被认为是抽象数据结构(ADT)
- 算法的一般操作:插入,查找,删除,迭代访问,排序
2.相关定义
- 1.数据库(database): 使用数据库术语表示在某一特定情况下所有要查询的数据
- 2.记录(record): 指数据库中划分的单元,它们为存储信息提供了一个结构格式
- 3.字段(filed): 一条记录经常被划分为几个字段,一个字段保存了某一种特定个的数据
2.数组
1.数组定义:
- 数组是内存中一块连续的存储空间,用来存储一组相同类型的数据
2.数组创建:
- 基本语法:
//方式一:先声明再指明数组空间的大小 数据类型[] 数组名; 数组名 = new 数据类型[数组空间大小]; //例: int[] Array; Array = new int[100];
//方式二:声明的同时指明数组空间的大小 数据类型[] 数组名 = new 数据类型[数组空间大小]; 例: int[] Array = new int[100];
//方式三:创建数组的同时进行初始化 数据类型[] 数组名 = new 数据类型[]{值1,值2,值3,...,值n}; 例: int[] Array = new int[]{1,2,3,4,5};
//方式四:创建数组的同时进行初始化 数据类型[] 数组名 = {值1,值2,值3,...,值n}; 例: int[] Array = {1,2,3,4,5};
- 语法说明:
- 1.Java中数组是引用数据类型,因此创建数组时必须使用new操作符
- 2.
[]
操作符对于编译器是一个标志,它说明正在命名的是数组对象而不是普通的变量- 3.推荐将[]放在数据类型后面,这样可以清楚地说明
[]
是数据结构的一部分而不是变量名的一部分public static void main(String[] args) { // TODO Auto-generated method stub int[] arr1 = new int[] {1,2,3,4,5}; //推荐 int [] arr2 = new int [] {1,2,3,4,5}; int arr3[] = new int[] {1,2,3,4,5}; }
- 4.数组是引用数据类型,所以它的数组名只是数组的一个引用,它并不是数组本身,数组数据存储在内存的堆空间中,数组名仅仅保存着这个堆地址
- 5.数组有一个length字段,通过它可以得知当前数组空间大小(数据项的个数)
- 注意:
- 1.创建数组时必须指明数组长度,否则无法分配空间
- 2.数组的长度必须为整数
- 3.一旦创建数组,数组的大小便不可改变
- 4.方式三和方式四创建的数组,长度由{}中的元素个数决定,所以方式三中的[]不能再指明数组长度,且数组的声明和赋值同时进行
3.数组初始化及数据项访问:
- 1.创建数组之后,如果不另行指定,那么数组会存放数据的默认值,不同类型的数组默认值可能不一样
- 2.当数组并没有进行显示初始化时,数组中存放的是其数据类型的默认值;注意:默认值并不是初始化,只有第一次显式赋值才是初始化
例: public class ArrayTest_day06{ public static void main(String[] args){ byte[] arr1 = new byte[10]; short[] arr2 = new short[10]; int[] arr3 = new int[10]; long[] arr4 = new long[10]; float[] arr5 = new float[10]; double[] arr6 = new double[10]; char[] arr7 = new char[10]; String[] arr8 = new String[10]; Student[] arr9 = new Student[10]; System.out.println(arr1[6]); System.out.println(arr2[6]); System.out.println(arr3[6]); System.out.println(arr4[6]); System.out.println(arr5[6]); System.out.println(arr6[6]); System.out.println(arr7[6]); System.out.println(arr8[6]); System.out.println(arr9[6]); } } class Student{ private String name; private String sex; public Student(){ }; }
- 3.数组显式初始化及数据项访问都需要通过下标
- 基本语法:
数据项访问: 数组名[下标]; 数组显式初始化: 数组名[下标]=值; 例: public static void main(String[] args) { // TODO Auto-generated method stub int[] arr1 = new int[5]; //推荐 arr1[0] = 1; arr1[1] = 2; arr1[2] = 3; arr1[3] = 4; arr1[4] = 5; System.out.println(arr[0]); }
- 语法说明:
- 1.数组的下标从
0
开始,到数组长度-1
为止- 2.如果下标超出范围则会抛出数组下标越界异常
java.lang.ArrayIndexOutOfBoundsException//数组下标越界异常
4.数组遍历:
- 1.依次获取数组中每个元素的过程称为数组遍历
- 2.数组数据项通过
[]
中的下标来访问- 3.利用循环遍历数组的过程实际上就是循环下标的过程
public class ArrayTest_day06{ public static void main(String[] args){ int[] arr = new int[10]; for(int i=0; i<arr.length; i++){ arr[i] = i; } System.out.println(arr[6]); System.out.println(arr[10]); //System.out.println(arr[-1]); } }
5.数组实例
1.array.java
class ArrayApp{ public static void main(String[] args){ long[] arr; arr = new long[100]; int nElems = 0; int j; long searchKey; arr[0] = 77; arr[1] = 99; arr[2] = 44; arr[3] = 55; arr[4] = 22; arr[5] = 88; arr[6] = 11; arr[7] = 00; arr[8] = 66; arr[9] = 33; nElems = 10; for(j=0; j<nElems; j++){ System.out.print(arr[j] + " "); } System.out.println(""); searchKey = 66; for(j=0; j<nElems; j++){ if(arr[j] == searchKey){ break; } } if(j == nElems){ System.out.println("找不到" + searchKey); }else{ System.out.println("找到了" + searchKey); } searchKey = 55; for(j=0; j<nElems; j++){ if(arr[j] == searchKey){ break; } } for(int k=j; k<nElems; k++){ arr[k] = arr[k+1]; } nElems--; for(j=0; j<nElems; j++){ System.out.print(arr[j] + " "); } System.out.println(""); } }
语法说明:
- 1.使用计数器nElems作为数组长度
- 2.插入:
arr[0] = 77;
用变量nElems记录已经插入数据项的个数- 3.查找:变量searchKey保存了待查找的值,使用for循环一个个比较,如果循环变量j变化到最后一个数据项还没有匹配上,这个值就不在数组中
- 4.删除:从查找特定的数据项开始,找到该数据后,将所有下标比它大的数据向前移动一位
- 5.显示:使用for循环逐步读取数组中的每个数据项
2.LowArray.java
class LowArray{ private long[] a; public LowArray(int size){ a = new long[size]; } public void setElem(int index,long value){ a[index] = value; } public long getElem(int index){ return a[index]; } } class LowArrayApp{ public static void main(String[] args){ LowArray arr = new LowArray(100); int nElems = 0; int j; arr.setElem(0,77); arr.setElem(1,99); arr.setElem(2,44); arr.setElem(3,55); arr.setElem(4,22); arr.setElem(5,88); arr.setElem(6,11); arr.setElem(7,00); arr.setElem(8,66); arr.setElem(9,33); nElems = 10; for(j=0; j<nElems; j++){ System.out.print(arr.getElem(j) + " "); } System.out.println(""); int searchKey = 26; for(j=0; j<nElems; j++){ if(arr.getElem(j) == searchKey){ break; } } if(j == nElems){ System.out.println("找不到" + searchKey); }else{ System.out.println("找到了" + searchKey); } for(j=0; j<nElems; j++){ if(arr.getElem(j)==55){ break; } } for(int k=j; k<nElems; k++){ arr.setElem(k,arr.getElem(k+1)); } nElems--; for(j=0; j<nElems; j++){ System.out.print(arr.getElem(j) + " "); } } }
语法说明:
- 1.LowArray.java实际是将一个Java数组封装进LowArray类中,并将数组封装,只有通过LowArray类中的方法才可以访问它
3.HighArray.java
class HighArray{ private long[] a; private int nElems; public HighArray(int max){ a = new long[max]; nElems = 0; } public boolean find(long searchKey){ int j; for(j=0; j<nElems; j++){ if(a[j] == searchKey){ break; } } if(j == nElems){ return false; }else{ return true; } } public void insert(long value){ a[nElems] = value; nElems++; } public boolean delete(long value){ int j; for(j=0; j<nElems; j++){ if(value == a[j]){ break; } } if(j==nElems){ return false; }else{ for(int k=j; k<nElems; k++){ a[k] = a[k+1]; } nElems--; } return true; } public void display(){ for(int j=0; j<nElems; j++){ System.out.print(a[j] + " "); } System.out.println(); } } class HighArrayApp{ public static void main(String[] args){ int maxSize = 100; HighArray arr = new HighArray(maxSize); arr.insert(77); arr.insert(99); arr.insert(44); arr.insert(55); arr.insert(22); arr.insert(88); arr.insert(11); arr.insert(00); arr.insert(66); arr.insert(33); arr.display(); int searchKey = 35; if(arr.find(searchKey)){ System.out.println("找到了" + searchKey); }else{ System.out.println("找不到" + searchKey); } arr.delete(00); arr.delete(55); arr.delete(99); arr.display(); } }
6.有序数组
- 1.假设一个数组,其中的数据项按关键字升序(或降序)排列,这种数组被称为有序数组
- 2.有序数组可以通过二分查找提高查找的效率,但降低了插入操作的速度
- 3.有序数组的查找:从开始查找,当找到一个比待查数据大(或小)的数时就退出查找
- 4.有序数组的插入:首先找到正确的位置,刚好在稍小值的后面,稍大值的前面,然后将所有比待插入数据项大的值向后移动一位,最后将待插入数据项插入(方法二:数组扩容)
- 5.有序数组的删除:首先找到找到待删除的数据,然后像前移动所有比删除数据项下标大的数据项
1.二分查找
public int find(long searchKey){ int lowerBoud = 0; int upperBound = nElems - 1; int curIn; while(true){ curIn = (lowerBound + upperBound) / 2; if(curIn == searchKey){ return curIn; }else if(lowerBound > upperBound){ return nElems; }else { if(a[curIn]<searchKey) { lowerBound = curIn + 1; }else { upperBound = curIn - 1; } } } }
- 语法说明:
- 注意:二分查找用在有序数组中
- 1.设变量lowerBound和upperBound指向数组的第一个和最后一个非空数据项,通过这些变量确定查找searchKey数据项的范围
- 2.while循环中,当前下标被设置为这个范围的中间值
- 3.分析可能出现的情况:
- 1.curIn可能刚好指向所需的数据项,所以先看是否相等,如果相等则意味着找到了该数据项,直接返回它的下标curIn
- 2.每一轮循环将范围缩小一半,最终这个范围会小到无法再分割
- 3.判断如果lowerBound比upperBound大,则范围已经不存在(注意:当lowerBound等于upperBound时还剩一个数据项,所以还需要再一次循环比较)
- 4.当范围不再有效时停止查找,但没有找到所需的数据项,所以返回数据项总数nElems,由于数组的最后一个非空数据项的下标为nElems-1,所以下标nElems是无效的,类用户把这个值解释为没有找到特定数据项
- 5.如果curIn没有指向所需数据项,且范围仍足够大,此时需要将范围缩小一半,比较当前下标所指的a[curIn],即范围中部值与待查数据的值searchKey
- 6.如果searchKey较大,则应该将范围设为当前范围的后半部分,因此将lowerBound移到了curIn,实际上是将lowBound移动到了curIn后的一个单元,因为在循环的开始已经检查了curIn
- 7.如果searchKey比a[curIn]小,则应将范围设为当前范围的前半部,因此将upperBound移到curIn的前一个数据项
- 完整代码:
package test.com.wd.test; public class Test{ private long[] a; private int nElems; public Test(int max) { a = new long[max]; nElems = 0; } public int find(long searchKey){ int lowerBound = 0; int upperBound = nElems - 1; int curIn; while(true){ curIn = (lowerBound + upperBound) / 2; if(curIn == searchKey){ return curIn; }else if(lowerBound > upperBound){ return nElems; }else { if(a[curIn]<searchKey) { lowerBound = curIn + 1; }else { upperBound = curIn - 1; } } } } public void insert(long value) { int j; for(j=0; j<nElems; j++) { if(a[j]>value) { break; } } for(int k=nElems; k>j; k--) { a[k] = a[k-1]; } a[j] = value; nElems++; } public boolean delete(long value) { int j = find(value); if(j==nElems) { return false; }else { for(int k=j; k<nElems; k++) { a[k] = a[k+1]; } nElems--; return true; } } public void display() { for(int j=0; j<nElems; j++) { System.out.print(a[j]+" "); } System.out.println(); } public static void main(String[] args) { int maxSize = 20; Test test = new Test(maxSize); for(int i=0; i<10; i++) { test.insert((long) (Math.random()*10)); } test.display(); System.out.println("-----------------------------"); int searchKey = 8; if(test.find(searchKey)!=test.nElems) { System.out.println("成功找到"+searchKey); }else { System.out.println("不存在"+searchKey); } } }
2.二分查找的效率
- 语法说明:
- 1.如图设s表示步数,r表示范围:则r=2s
- 2.如果已知步数s,通过该方程可以得出范围(例:s是6,则范围是26,即:64以内的数,通过二分法只需要6次就可以n找到)
n- 3.指数函数的反函数- -对数:以n为底r的对数表示为了得到r而需要重复乘n的次数
- 4.若已知范围,求出完成搜索所需的步数(即已知的是r,希望有一个方程可以求出s),此时需要用到对数
- 5.表达式:s = log2( r )
- 6.注意:log一般都是以10为底来求对数,但是通过将结果乘以3.322可以转换成以2为底的对数(例:log10(100)=2,从而log10(100)=2乘以3.322;即:6.644,四舍五入为7)
- 7.所以一般二分查找的效率用大O表示法表示为:O(logN),其中N为范围
3.有序数组的优缺点:
优点:
- 1.查找速度比无序数组快的多
- 2.有序数组的二分查找在查找频繁的情况下十分有用,且数据越多,效果越明显
缺点:
- 1.插入操作中由于所有靠后的数据都需移动以腾来空间,所以速度较慢
- 2.有序数组和无序数组的删除操作都很慢,因为数据项必须向前移动来填补已删除数据项的洞
7.数组总结
1.无序数组的插入:常数
- 1.唯一 一个与数组数据项个数无关的算法,新数据项总是被放在下一个有空的地方(a[nElems]),然后nElems++
- 2.无论数组中的数据项个数N有多大,一次插入总是用相同的时间;即:无序数组中插入一个数据项的时间T是一个常数K*1
- 3.表达式:
T = K * 1
;- 4.K:实际情况下插入所需的时间K,与很多因素有关(微处理器,编译程序生成程序代码的效率等),由于大O表示法只关注算法效率与数据项的个数的关系,所以无序数组的效率为:O(1)
2.无序数组的查找(线性查找):与N成正比
- 1.数组数据项的线性查找中,查找特定数据项所需的比较次数平均为数据项总数的一半,因此设N为数据项总数,搜索时间T与N的一半成正比
- 2.表达式:T = K*N/2
- 3.可以将2并入K中得到化简后的公式:新K值等于原K/2
- 4.表达式:T = K * N
- 5.这个方程说明平均线性查找时间与数组的大小成正比,用大O表示法为:O(N)
3.二分查找:与log(N)成正比
- 1.二分查找定义一个与T和N有关的公式为:
- 2.表达式:T = K*log2(N)
- 3.时间T与以2为底N的对数成正比,实际上由于所有的对数和其他对数成比例(从底数为10转换到底数为2需乘以3.322),我们可以将这个为常数的底数并入K,由此不必指定底数
- 3.表达式:T = K*log(N)
- 4.大O表示法省去了常数K,当比较算法时,它并不在乎具体的微处理器芯片或编译器,真正需要比较的是对应不同的N值,T是如果变化的,而不是具体的数字,因此不需要常数
- 5.即:二分查找使用了O(log N)级时间
8. 大O表示法
- 1.描述算法的速度与数据项的个数的联系
- 2.O(1)优秀,O(logN)良好,O(N)可以,O(N2)略差
- 3.大O表示法的实质并不是对运行时间给出实际值,而是表达了运行时间是如何受数据项个数所影响的
简单排序
1.冒泡排序
- 1.排队(从底到高):
- 1.从队列的最左边开始,比较0号位置和1号位置的队员,如果左边的队员高,就让两个队员交换位置,如果右边的队员高就什么也不做(从小到大排列,如果想从大到小排列,则操作相反)
- 2.按照步骤1一直比较,一直比较到队列的最右端,虽然还没有完全把所有队员都排好序,但是最高的队员已经被排在最右边了(因为在每次比较两个队员的时候,只要遇到最高的队员就会交换他的位置,直到最后他到达队列的最右边)
- 3.对所有队员进行了第一趟排序之后,进行了N-1次比较,并且按照队员的初始位置,进行了最少0次,最多N-1次交换,数组最末端的那个数据项就此排定,不需要再移动了
- 4.重新回到队列的最左端开始进行第二趟排序,再一次地从左到右,两两比较,并且在适当的时候交换队员的位置,这次只需比较到右边的第二个队员(位置N-2),因为最高的队员已经占据了最后位置,即N-1号位置
- 5.不断执行这个过程,直到所有的队员都排定
- 2.规则:
- 1.比较两个队员
- 2.如果左边的队员高,则两队员交换位置(从小到大)
- 3.向右移动一个位置,比较下面两个队员
- 4.当碰到第一个排定的队员后,就返回到队列的左端重新开始下一趟排序
- 5.不断执行这个过程,直到所有的队员都排定
- 3.冒泡排序及其优化的完整代码:
package chapter2; import java.util.Arrays; /** * 冒泡排序及其优化 */ public class BubbleSort { static int[] arr = new int[] {2,1,3,4,5}; public static void main(String[] args) { //普通一:从小到大 //bubbleSort1(arr); //System.out.println(Arrays.toString(arr)); //普通一:从大到小 //bubbleSort2(arr); //System.out.println(Arrays.toString(arr)); //普通二:从小到大 //bubbleSort3(arr); //System.out.println(Arrays.toString(arr)); //优化一:从小到大,当排序已经完成时,让排序提前结束,避免多余的循环 //optimizedBubbleSort1(arr); //System.out.println("最终结果:"+Arrays.toString(arr)); optimizedBubbleSort3(arr); System.out.println("最终结果:"+Arrays.toString(arr)); //optimizedBubbleSort4(arr); //System.out.println("最终结果:"+Arrays.toString(arr)); } //普通一:从小到大 public static void bubbleSort1(int[] arr) { //外层循环:要遍历的轮数,为n-1轮 for(int i=arr.length-1; i>0;i--) { //内层循环:要比较的次数,最初为n-1次,每一轮次数减一 for(int j=0; j<i; j++) { //判断:是否交换 if(arr[j]>arr[j+1]) { swap(j,j+1); } } } } //普通一:从大到小 public static void bubbleSort2(int[] arr) { //外层循环:要遍历的轮数,为n-1轮 for(int i=arr.length-1; i>0;i--) { //内层循环:要比较的次数,最初为n-1次,每一轮次数减一 for(int j=0; j<i; j++) { //判断:是否交换 if(arr[j]<arr[j+1]) { swap(j,j+1); } } } } //普通二:从小到大,从大到小与方式一相同 public static void bubbleSort3(int[] arr) { //外层循环:要遍历的轮数,为n-1轮 for(int i=1; i<arr.length; i++) { //内层循环:要比较的次数,最初为n-1次,每一轮次数减一 for(int j=0; j<arr.length-i; j++) { if(arr[j]>arr[j+1]) { //判断:是否交换 swap(j,j+1); } } } } //优化一:当排序已经完成时,让排序提前结束,避免多余的循环 //解决方法:设置一个标志位,用来表示第i遍是否有交换,如果有则要进行第i+1遍;如果没有,则说明当前数组已经完成排序,跳出循环 public static void optimizedBubbleSort1(int[] arr) { for(int i=1; i<arr.length; i++) { //设置一个标志位,用来表示第i遍是否有交换,如果有则要进行第i+1遍;如果没有,则说明当前数组已经完成排序,跳出循环 boolean flag = false; for(int j=0; j<arr.length-1; j++) { if(arr[j]>arr[j+1]) { swap(j,j+1); flag = true; } System.out.println("第"+i+"遍第"+(j+1)+"次比较的排序结果: "+Arrays.toString(arr)); } System.out.println("第"+i+"遍最终排序结果: "+Arrays.toString(arr)); if(flag == false) { break; } } } //优化二:在优化一的基础上保存最后交换的下标,避免中间多余的比较 public static void optimizedBubbleSort2(int[] arr) { for(int i=1; i<arr.length; i++) { //设置一个标志位,用来表示第i遍是否有交换,如果有则要进行第i+1遍;如果没有,则说明当前数组已经完成排序,跳出循环 boolean flag = false; //设置每次循环的截止的最后一个下标,假如后面已经有序,可以避免多余的比较 int temp = arr.length-i; for(int j=0; i<temp; j++) { if(arr[j]>arr[j+1]) { swap(j,j+1); flag = true; temp = j; } System.out.println("第"+i+"遍第"+(j+1)+"次比较的排序结果: "+Arrays.toString(arr)); } System.out.println("第"+i+"遍最终排序结果: "+Arrays.toString(arr)); if(flag == false) { break; } } } //优化三:鸡尾酒排序(又称搅拌排序)+优化一,其中鸡尾酒排序的元素比较和交换过程是双向的;即:一轮比较会排出一个最大值和一个最小值,然后在第二个和第N-1个元素中排出一个第二大和一个第二小值,依次进行 public static void optimizedBubbleSort3(int[] arr) { //外层循环:当数组的长度为偶数时或奇数都需要arr.length/2轮循环,因为为奇数时,比较完上面和下面对称的部分后,剩下中间的一个会自动排好序 for(int i=1; i<arr.length/2+1; i++) { //设置一个从左往右比较的标志位,用来表示从左往右比较第i遍是否有交换,如果有则要进行第i+1遍;如果没有,则说明当前数组已经完成排序,跳出循环 boolean isRightExchange = false; for(int j=0; j<arr.length-i; j++) { if(arr[j]>arr[j+1]) { swap(j,j+1); isRightExchange = true; } System.out.println("上第"+i+"遍第"+(j+1)+"次比较的排序结果: "+Arrays.toString(arr)); } System.out.println("上第"+i+"遍最终排序结果: "+Arrays.toString(arr)); if(!isRightExchange) { break; } //设置一个从右往左比较的标志位,用来表示从右往左比较第i遍是否有交换,如果有则要进行第i+1遍;如果没有,则说明当前数组已经完成排序,跳出循环 boolean isLeftExchage = false; for(int k=arr.length-i-1; k>i-1; k--) { if(arr[k]<arr[k-1]) { swap2(k,k-1); isLeftExchage = true; } System.out.println("下第"+i+"遍第"+(arr.length-k-1)+"次比较的排序结果: "+Arrays.toString(arr)); } System.out.println("下第"+i+"遍最终排序结果: "+Arrays.toString(arr)); if(!isLeftExchage) { break; } } } //优化四:鸡尾酒排序+优化一+优化二 public static void optimizedBubbleSort4(int[] arr) { for(int i=1; i<arr.length/2+1; i++) { boolean isRightExchange = false; //设置一个下标,用来记录每轮循环比较的最后一位的下标,避免局部有序还进行多余比较的情况 int temp1 = arr.length - i; for(int j=0; j<temp1; j++) { if(arr[j]>arr[j+1]) { swap(j,j+1); isRightExchange = true; temp1 = j; } System.out.println("上第"+i+"遍第"+(j+1)+"次比较的排序结果: "+Arrays.toString(arr)); } System.out.println("上第"+i+"遍最终排序结果: "+Arrays.toString(arr)); if(!isRightExchange) { break; } boolean isLeftExchange = false; int temp2 = i-1; for(int k=arr.length-i-1; k>temp2; k--) { if(arr[k]<arr[k-1]) { swap(k,k-1); isLeftExchange = true; temp2 = k; } System.out.println("下第"+i+"遍第"+(arr.length-k-1)+"次比较的排序结果: "+Arrays.toString(arr)); } System.out.println("下第"+i+"遍最终排序结果: "+Arrays.toString(arr)); if(!isLeftExchange) { break; } } } //交换数据:注意引用数据类型的交换可以使用该方法,基本数据类型不适用 public static void swap(int i,int j) { int temp = 0; temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } //交换数据优化:冒泡排序里面需要交换两个元素的位置,交换两个元素可以用异或的操作,这样不需要创建第三个变量,减少内存开销。 public static void swap2(int i,int j) { arr[i] ^= arr[j]; arr[j] ^= arr[i]; arr[i] ^= arr[j]; } }
- 语法说明:
- 1.普通一思路:
- 1.将最小的数据项放在数组的最开始(数组下标为0),并将最大的数据项放在数组的最后(数组下标为nElems-1)
- 2.外层for循环的计数器i从最后开始,即
i
等于nElems-1,每经过一次循环i
减一,下标大于i
的数据项都已经是排好序的了,变量i
在每完成一次内部循环就左移一位,因此算法就不再处理那些已经排好序的数据- 3.内层for循环计数器j从数组的最开始0算起,每完成一次内部循环体加一,当它等于i时结束一次循环,在内层for循环体中,比较数组下标为
j
和j+1
的两个数据项,如果下标为j
的数据项大于下标为j+1
的数据项,则交换两个数据项- 2.swap()方法:
- 1.为了清晰起见,使用了一个独立的swap()方法来执行交换操作;它只是交换数组中的两个数据项的值,使用临时变量来存储第一个数据项的值,然后把第二项的值赋给第一项,之后让第二项的值等于临时变量的值
- 2.实际上使用一个独立的swap()方法不一定好,因为方法调用会增加一些额外的消耗,最好直接放到程序中可以提高一些速度
- 3.不变性:
- 1.许多算法中,有些条件在算法执行时是不变的,这些条件被称为不变性,可以反复验证不变性是否为真判断是否出错
- 2.冒泡排序中的不变性是指
i
右边的所有数据项为有序,这个条件在算法运行过程中始终为真- 4.冒泡排序的效率:
- 1.以10个数据为例:第一趟进行了9次比较,第二趟进行了8次比较,以此类推,直到最后一趟进行了一次比较
- 2.对于10个数据项,就是:9+8+7+6+5+4+3+2+1=45
- 3.一般来说,数组中有N个数据项,则第一趟排序中有N-1次比较,第二趟中有N-2次,如果类推:(N-1)+(N-2)+(N-3)+…+1 = N*(N-1)/2
- 4.即:算法约做了N2次比较(可以忽略-1,不会有很大差别,特别是当N很大时)
- 5.因为两个数据只有在需要时才交换,所以交换的次数少于比较的次数,如果数据是随机的,那么大概会有一半数据需要交换,则交换次数为N2/4(不过在最坏的情况下,即数据逆序时,每次比较都需要交换)
- 6.交换和比较操作的次数都和N2成正比,由于常数不算在大O表示法中,可以忽略2和4,所以认为冒泡排序运行需要O(N2)时间级别
2.选择排序
- 选择排序改进了冒泡排序,将必要的交换次数从O(N2)减少到O(N)但是比较次数没有改变
- 原理:进行选择排序就是把所有队员扫描一遍,从中选择最矮的一个队员,最矮的队员和站在队列最左端的队员交换位置,即站到0号位置,现在最左端的队员是有序的了,不需要再交换位置了,一般有序的队员都排在队列的左边(较小的下标值)
- 再次扫描球队队列,从一号位置开始,还是寻找最矮的,然后和一号位置的队员交换,直到所有队员都排定
- 排序从球员队列的最左端开始,在记录本上记录下最左端球员的身高,并且把紫红色的毛巾放在这个队员的前面,于是开始用下一个球员的升高和记录本上记录的值相比较,如果这个队员更矮,则划掉第一个球员的升高,记录下第二个队员的身高;同时移动毛巾,把它放在这个新的最矮的队员前面,继续沿着队列走下去,每一个队员都和记录本上的最小值进行比较,当发现更矮的队员时,就更新记录本上的最小值并且移动毛巾,将这个最矮的队员和队列最左边的队员交换位置,现在已经对一个队员排好了序,这期间做了N-1次比较,但是只进行了一次交换,在下趟的排序中,所做的事情是一样的,只是要忽略最左边的队员,因为他已经排定了,因此算符的第二趟排序从位置1而不是从位置0开始,每进行完一趟排序,就多一个队员有序,并被安排在左边,依次排序
package chapter2; import java.util.Arrays; /** * 选择排序及其优化 * @author 86158 * @since 20210808 */ public class SelectionSort { static int[] arr = new int[] {10,2,4,3,8,6,0,22,34,6,9}; public static void main(String[] args) { // TODO Auto-generated method stub //selectionSort1(arr); //System.out.println(Arrays.toString(arr)); //selectionSort2(arr); //System.out.println(Arrays.toString(arr)); //optimizedSelectionSort1(arr); //System.out.println(Arrays.toString(arr)); //optimizedSelectionSort2(arr); //System.out.println(Arrays.toString(arr)); optimizedSelectionSort2(arr); System.out.println(Arrays.toString(arr)); } //普通一: public static void selectionSort1(int[] arr) { for(int i=0; i<arr.length; i++) { int n = 0; for(int j=i+1; j<arr.length; j++) { if(arr[i]>arr[j]) { swap(i,j); n++; System.out.println("第"+(i+1)+"遍第"+n+"次交换:" + arr[i]+"和"+arr[j]); } } } } //普通二: public static void selectionSort2(int[] arr) { int i,j,min; for(i=0; i<arr.length; i++) { min = i; for(j=i+1; j<arr.length; j++) { if(arr[min]>arr[j]) { min = j; } } swap(i,min); System.out.println("第"+(i+1)+"遍第"+(i+1)+"次交换:"+arr[i]+"和"+arr[min]); } } //优化一:每一趟先选出最小值再选出最大值,把最小值和第一个数据交换,最大值和最后一个数据交换 public static void optimizedSelectionSort1(int[] arr) { int i,j,min,max; for(i=0; i<arr.length/2; i++){ min = i; max = arr.length-i-1; for(j=i+1; j<arr.length-i; j++) { if(arr[min]>arr[j]) { min = j; } } swap(i,min); System.out.println("前第"+(i+1)+"遍第"+(i+1)+"次交换:"+arr[i]+"和"+arr[min]); for(j=i+1; j<arr.length-i; j++) { if(arr[max]<arr[j]) { max = j; } } swap(arr.length-i-1,max); System.out.println("后第"+(i+1)+"遍第"+(i+1)+"次交换:"+arr[arr.length-i-1]+"和"+arr[max]); } } //优化二:每一趟同时选出最小值和最大值,把最小值和第一个数据交换,最大值和最后一个数据交换 public static void optimizedSelectionSort2(int[] arr) { int i,j,min,max; //外层循环:比较的趟数,由于每次同时选出一个最大值和最小值,所以只需要arr.length/2趟 for (i=0; i<arr.length/2; i++) { // 记录最小值,从0开始,每次向后移动一位 min = i; // 记录最大值,从最后一个下标开始,每次向前移动一位 max = arr.length-i-1; //内层循环,比较每一轮找出最大值和最小值的下标保存在max和min变量中,从i开始,不能从i+1开始,否则会错误 for (j = i; j < arr.length-i-1; j++) { if (arr[j]<arr[min]) { min = j; } if (arr[max]<arr[j]) { max = j; } } System.out.println("最小值:"+min+"和"+i+"交换"); swap(i, min); //注意:先对最小值进行了交换,如果最大值刚好在最小值的位置,则最大的位置会变成最小值的位置,所以需要将最小值下标给最大值下标 if (i == max) { max = min; } System.out.println("最大值:"+max+"和"+(arr.length-i-1)+"交换"); swap(arr.length-i-1, max); } } public static void swap(int i,int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
- 不变性:
- 在selectionSort中,下标小于或等于i的位置的数据项总是有序的
- 选择排序的效率
- 选择排序和冒泡排序执行了相同次数的比较:N*(N-1)/2,但是选择排序进行的交换更少,选择排序的比较运行了O(N2)时间,交换则运行了O(N)时间
3.插入排序
- 插入排序仍然需要O(N2)时间,但是在一般情况下,它要比冒泡排序快一倍,比选择排序还要快一点
- 排队:
- 局部有序,在队伍的中间有一个作为标记的队员,在这个队员左边的所有队员已经是局部有序的,这意味着这部分人之间是按顺序排序的,然后这些队员在队列中最终的位置还没有确定,因为当后面没有被排过序的队员要插入到它们中间时,它们的位置还要发生变动
- 被标记的队员他和他右边的队员都是未排过序的,然后在左边局部有序的适当位置插入被标记的队员,需要把部分已排序的队员右移以腾出空间,为了提供移动所需的空间,需要先让被标记的队员出列,在程序中,这个数据项被存储在一个临时变量中
- 现在移动已经排过序的队员来腾出空间,将局部有序中的最高的队员移动到原来被标记队员所在位置,次高的队员移动到原来的队员所在的位置,依次类推
- 被标记的队员和每一个要移动的队员比较身高,当吧最后一个比被标记的队员还高的队员移位之后,这个移动的过程就停止了,最后一次移动空出的位置,就是被标记队员应该插入的位置
- 结果:局部有序的部分多了一个队员,而未排序的部分里少了一个队员,被标记的位置向右移动一位,仍然放在未排序部分的最左边的队员前面,重复这个过程,直到所有未排序的队员都被插入到局部有序队列中合适的位置
package chapter2; import java.util.Arrays; /** * 插入排序及其优化 * @author 86158 * @since 20210809 */ public class InsertionSort { static int[] arr = new int[] {1,5,7,12,6,24,11,55,33,2}; public static void main(String[] args) { // TODO Auto-generated method stub //insertionSort(arr); //System.out.println(Arrays.toString(arr)); optimizedInsertionSort(arr); System.out.println(Arrays.toString(arr)); } //普通一: public static void insertionSort(int[] arr) { //外层循环:假定除第一个元素有序外,其他元素均无序,所以需要arr.length-1趟插入 for(int i=1; i<arr.length; i++) { //定义一个变量保存第二个元素的值 int temp = arr[i]; //定义一个变量限定内层循环的条件 int j = i; //从第二个元素开始,如果前一个大于该元素,就将该元素向后移动一位,直到小于或者到第一个元素为止 while(j>0 && arr[j-1]>=temp) { System.out.println(arr[j-1]+"移动到"+arr[j]); arr[j] = arr[j-1]; //向前移动一位重新判断 j--; } System.out.println("将"+temp+"插入到"+arr[j]); //将保存在temp中的值插入到找到的j位置上 arr[j] = temp; } } //优化一:查找插入位置的时候,用二分查找提高查找 public static void optimizedInsertionSort(int arr[]) { int i,j,lowerBound,upperBound,curIn; //外层循环,假定除第一个元素外,其他元素均无序,需要对第二个及后面的数据进行插入排序 for(i=1; i<arr.length; i++) { //用一个变量保存无序部分的第一个数据值 int temp = arr[i]; //用二分查找无序部分的第一个数据值应该插入到有序部分的哪个位置,所以lowerBound从0开始,upperBound从无序部分的第一个数组值的前一个数据(即:有序部分的最后一个数据) lowerBound = 0; upperBound = i - 1; //二分法查找合适的插入位置,结果为upperBound+1 while(lowerBound<=upperBound) { curIn = (lowerBound+upperBound)/2; if(arr[curIn]<temp) { lowerBound = curIn+1; }else { upperBound = curIn-1; } } //将i-1(大)到upperBound+1(小)的数据都向后移动一位 for(j=i-1; j>=upperBound+1; j--) { arr[j+1] = arr[j]; } //循环过后j--了一次,所以需要将j+1才能将temp插入到正确的upperBound+1的位置上 arr[j+1] = temp; } //优化二:希尔排序 } }
- 插入排序的不变性:
- 在每趟结束时,在将temp位置的项插入后,比i变量下标号小的数据项都是局部有序的
- 插入排序的效率:
- 第一趟排序中,它最多比较一次,第二趟最多比较两次,最后一趟,比较N-1次
- 因此:1+2+3+…+N-1=N*(N-1)/2
- 然而在每一趟排序发现插入点之前,平均只有全体数据项的一半真的进行了比较,我们除以2得到N*(N-1)/4
- 复制的次数大致等于比较的次数,然而一次复制与一次交换的时间耗费不同,所以相对于随机数据,这个算法比冒泡排序快一倍,比选择排序略快
- 插入排序也需要O(N2)的时间段
- 对于基本有序或者已经有序的数据来说,插入排序要好的多,当数据有序时,while循环的条件总是假,所以它变成了外层循环中的一个简单语句,执行N-1次,在这种情况下,算符运行值只需要O(N)的时间,如果数据基本有序,插入排序几乎只需要O(N)的时间,这对把一个基本有序的文件进行排序是一个有效的方法,但是对于逆序排列的数据,每次比较和移动都会执行,所以插入排序不比冒泡排序快
4.对象排序
##5.简单排序的比较总结
高级排序
1.希尔排序
- 希尔排序基于插入排序,也可以说是插入排序的优化
- 增加了一个新的特性,大大地提高了插入排序的执行效率
- 依靠这个特别的实现机制,希尔排序对于多达几千个数据项,中等大小规模的数组排序表现良好。
- 希尔排序不像快速排序和其他时间复杂度为O(N*logN)的排序算符那么快,因此对非常大的文件排序,它不是最优选择,但是,希尔排序比选择排序和插入排序这种时间复杂度为O(N2)的排序要快的多,而且它非常容易实现
- 它在最坏情况下的执行效率在和平均情况下的执行效率相比并没差很多
- 插入排序:复制的次数太多
- 由于希尔排序是基于插入排序的,在插入排序执行的一半的时候,标记符左边这部分数据项都是排过序的,而标记右边的数据项项则没有排过序
- 这个算符取出标记符所指的数据项,把它存储在一个临时变量里,然后从那个刚刚被移除的数据项的左边的第一个单元开始,每次把有序的数据项向右移动一个单元,直到存储在临时变量里的数据项能够有序回插
- 问题:假如一个很小的数据项在很靠近右端的位置上,这里本来应该是值比较大的数据项所在的位置,把这个小数据项移动到在左边的正确的位置上,所有的中间数据项(这个数据项原来所在位置和它应该移动到的位置之间的数据项)都必须向右移动一位,这个步骤对每一个数据项都执行了将近N次的赋值,虽然不是所有数据项项都必须移动N个位置,但是数据项平均移动了N/2个位置,这就执行了N次N/2个移位,总共是N2/2次复制,因此,插入排序的执行效率是O(N2)
- 如果能以某种方式不必一个一个地移动所有中间的数据项,就能把较小的数据项移动到左边,那么这个算法的执行效率就会有很大的改进
- n-增量排序
- 希尔排序通过加大插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能大跨度地移动,当这些数据项排过一趟序后,希尔排序算法减少数据项的间隔再进行排序,依次进行下去,进行这个排序时数据项之间的间隔被称为增量,并且习惯上用字母h来表示
例:增量为4时对包含10个数据项的数组进行排序的一个步骤的情况:- 首先是0,4,8,进行排序,然后算符向右移动一位,对1,5,9数据项进行排序,这个排序过程持续进行,直到所有的数据项都已经完成4-增量排序,也就是说所有间隔为4的数据项之间都已经排列有序
- 在完成以4为增量的希尔排序之后,数组可以看成是由4个子数组组成:(0,4,8),(1,5,9),(2,6),(3,7),这4个子数组内分别是完全有序的,这些子数组相互交错排列,然后彼此独立
在完成以4为增量的希尔排序后,所有元素离它在最终有序序列中的位置相差都不到两个单元,这就是数组基本有序的含义,也正是希尔排序的奥秘所在。- 通过创建这种交错的内部有序的数据项集合,把完成排序所必须的工作量将到最小
- 减少间隔
- 除了4个间隔外,对于更大的数组,开始的间隔也应该更大,然后间隔不断缩小,直到间隔变成1
- 例:含有1000个数据项的数组可能先以364为增量,然后以121为增量,以40位增量,以13为增量,以4为增量,最后以1为增量进行希尔排序
- 用来形成间隔的数列(…364,121,40,13,4,1)被称为间隔序列,这里所表示的间隔序列由Knuth提出,此序列常用,数列以逆向的形式从1开始,通过递归表达式h = 3*h + 1来产生,初始化值为1
- 当然还有其他一些方法也能产生间隔序列
- 在排序算符中,首先在一个短小的循环中,使用序列的生成公式来计算出最初的间隔,h值的最初被赋为1,然后应用公式h = 3*h+1生成序列,1,4,13,40,121,364,等等,当间隔大于数据大小的时候过程停止,
- 对于一个含有1000个数据项的数组,序列的第七个数字,1093就太大了,因此,使用序列的第6个数字作为最大的数字来开始这个排序过程,作-364增量排序,然后每完成一次排序例程的外部循环,用前面提供的次公式的倒推式来减少间隔
- h = (h-1)/3
- 这个倒推的公式生成逆置的序列364,121,40,13,4,1,从364开始,以每个数字作为增量进行排序,当数组用1-增量排序后,算法结束
哈希表
哈希表(Hash table,也叫散列表),是根据关键码(Key value)而直接进行访问的数据结构,即它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫散列函数,存放记录的数组叫做散列表
记录的存储位置=f(关键字)
这里的对应关系f称为散列表,又称为哈希(Hash)函数,采用散列表技术将记录存储再一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)
哈希表
哈希表是一种数据结构,它可以提供快速的插入操作和查找操作
不论哈希表中有多少数据,插入和删除(有时候包括删除)只需要接近常量的时间:即O(1)的时间级,实际上,这只需要几条机器指令
如果需要在一秒钟内查找上千条记录,通常使用哈希表(例如:拼写检查器)
哈希表的速度明显比树快
哈希表的缺点:哈希表是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,,所以程序员必须要清楚表中要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是一个费时的过程)
而且也没有一种简便的方法可以以任何一种顺序(例如从小到大)来遍历表中数据项,如果需要这种能力,就只能选择其他数据结构
如果不需要有序遍历数据,并且可以提前预测数据量的大小,那么哈希表在速度和易用性上是无与伦比的
关键;如何把关键字转换成数组下标
在哈希表中,这个转换通过哈希函数来完成,然而对于特定的关键字并不需要哈希函数:关键字的值可以直接用于数组下标
关键字作为索引
有序合理,且没有删除
所以序列中不存在浪费内存的断裂带,新数据可以添加在数组的尾部,且数组容量不需要比当前数据项的数量大很多
问题
当关键字组织的并并不合理有序
字典:
前面描述的雇员信息数据库的关键字表现很好,,而在其他应用中,情况并非如此,经典的例子就是字典,如果想要把一本英文字典的每个单词都写入计算机内存,以便快速读写,哈希表是一个好的选择
哈希表还在高级计算机语言的编译器中用到,他们通常用哈希表保留符号表,符号表记录了程序员声明的所以变量和函数名,以及它们在内存中的地址,程序中需要快速访问这些名字,所以哈希表是理想的数据结构
例如,想在内存中存储50000个英文单词,起初可能考虑每个单词占据一个数组单元,那么数组的大小是50000,同时可以使用数组下标存取单词,这样,存取确实很快,但是数组下标和单词有什么关系呢,录入随意给出一个单词,怎么快速找到它的数组下标
把单词转换为数组下标
现在需要的是把单词转换为适当下标的系统,现在已经知道,计算机应用不同的编码方案,用数字代表单个的字符,其中一种是ASCII编码,其中a是97,b是98,依次类推,直到122代表z,然而,ASCII码从0到255,可以容纳字母,标点等字符,英文单词中只有26个字符,所以可以设置出一种自己的编码方案,它可以潜在的存储内存空间
把数字相加
幂的连乘
哈希化
需要一种压缩方法,把数位幂的连乘系统中得到的巨大的整数范围压缩到可接受的数组范围中
对于英语词典,多大的数组才合适?如果只有50000个单词,可能会假设这个数组大概就有这么多空间,但实际上,需要多一倍的空间容纳这些单词,所以,最终需要容量为100000的数组
现在就找到一种方法,把0到超过70000000000的范围,压缩为从0到100000
有一种简单的方法就是使用取余操作符,它的作用是得到一个数被另一个数整除后的余数
假设把0到199的数字(用变量largeNumber代表),压缩为从0到9的数字(用变量smallNumber代表),后者有10个数,所以说变量smallRange值为10,而变量largeNumber的值是多少并不重要(除非内存溢出),这个Java
表达式为:smallNumber = largeNumber % smallRange;
当一个数被10整除时,余数一定在0到9之间,这样,就把0-199的范围压缩到0-9的范围,压缩率为20:1
也可以用类似的方法把表示单词的唯一的数字压缩城数组的下标:
arrayIndex = hugeNumber%arraySize
这就是一种哈希函数,它把一个大范围的数字哈希(转化)成一个小范围的数字,这个小范围对应这数组的下标,使用哈希函数向数组插入数据后,这个数组就称为哈希表
通过把单词每个字母乘以27的适当次幂,使单词称为了一个巨大的数字。
然后使用取余操作符(%),把得到的巨大整数范围转化成两倍于要存储内容的数组下标范围
arraySize = numberWords * 2;
arrayIndex = hugeNumber % arraySize;
冲突
把巨大的数字空间压缩成较小的数字空间,必然要付出代价,即不能保证每个单词都映射到数组的空白单元
这样,不可能避免的把几个不同的单词哈希化到同一个数组单元,当然希望每个数组下标对应一个数据项,但是这通常不太可能
假设在数组中插入一个单词,通过哈希函数得到了它的数组下标后,发现那个单元已经有一个单词了,因为这个单词哈希化后得到的数组下标相同,这种情况称为冲突
冲突的可能性会导致哈希化方案无法实施,实际上,可以通过其他方式解决这个问题,前面说过指定的数组大小两倍于需要存储的数据量,因此,可能一半的单元使空的,当冲突发生时,一个方法使通过系统的方法找到数组的一个空位,并把这个单词填入,而不再用哈希函数得到数组下标,这个方法叫做:开放地址法
第二种方法:使创建一个存放单词链表的数组,数组内不直接存储单词,这样,当冲突发生时,新的数据项直接接到这个数组下标所指的链表中。这种方法叫做:链地址法
开放地址法:在开放地址法中,若数据不能直接放在由哈希函数计算出来的数组下标所指的单元时,就要寻找数组的其他位置,下面要探索开放地址法的三种方法:它们在找下一个空白单元时使用的方法不同,这三种方法分别时线性探测,二次探测和再哈希法
线性探测
在线性探测中,线性地查找空白单元,如果5421是要插入数据的位置,它已经被占用了,那么就使用5422,然后依次类推,数组下标一直递增,直到找到空位,这就叫线性探测,因为它沿着数组的下标一步一步顺序地查找空白地单元
哈希表中,一串连续地已填充单元叫做填充序列,增加越来越多地数据项时,填充序列变地越来越长,这叫做聚集
注意,如果把哈希表填地太满,那么在表中每填入一个数据项都需要花费很长时间,可能会认为程序已经停止,哈希表在机会被填满地数组中添加数据项,效率很低
而且,如果哈希表被完全填满,算法就会停止工作
一个关键字应用哈希函数,结果是一个数组下标
这个下标所指地单元可能是要寻找地关键字,这是最好地情况,并且立即报告查找成功
然而,这个单元可能被其他关键字占据,这就是冲突;就会看到箭头指向一个被占用地单元,根据冲突地位置,查找算法依次查找下一个单元,查找合适单元地过程叫做探测
根据冲突地位置,查找算符只是沿着数组一个一个地查看每个单元,如果在找到要寻找地关键字前遇到一个空位,说明查找失败,不需要再做查找,因为插入算法本应该把这个数据项插在那个空位上,
大多数数据会一次插入成功,但有些会遇到冲突,需要沿数组步行,查找新地空白单元,它们走过地步数成为探索长度
线性探测地删除:删除一个关键字,不是简单地把某个单元地数据项删除,把它变成空白,因为在插入操作中,探测过程走过一系列单元,查找一个空白单元,如果在一个填充序列中间有一个空位,查找算法就会看在看到它时中途放弃查找,即使最终本可以到达要求地那个单元
因此,要用一个有特殊关键字值地数据项代替要被删除地数据项,以此标识此处地数据已不存在,假设所有有效关键字值都是正数,所以被删除地数据变成-1,被删除地数据项用特殊地关键字(DEL)标识
链地址法:
开放地址法中,通过在哈希表中再寻找一个空位解决冲突问题,另一个方法是在哈希表每个单元中设置链表,某个数据项地关键字值,还是像通常一样映射到哈希表地单元,而数据项本身插入到这个单元地链表中,其他同样映射到这个位置地数据项只需要加到链表中,不需要在原始地数组中寻找空位
链地址法在概念上比开放地址法中地几种探测策略要简单,然而代码会比其他地长,因为必须要包含链表机制,这就要在程序中增加一个类