JAVA数据结构和算法:第八章(排序)
来源:互联网 发布:vb系列振动电机 编辑:程序博客网 时间:2024/06/05 10:20
#排序
排序是我们程序中经常面对的问题,那么排序的严格定义是什么呢?
假设含有n个记录的序列为{r1,r2,r3…..,rn},其对应的关键字分别为{k1,k2,k3…..,kn},需确定1,2,…..,n的一种排列p1,p2,……..,pn,使其相应的关键字满足Kp1<=Kp2…….<=Kp2(非递减或非递增)关系,即使得序列成为一个按关键字有序的序列{rp1,rp2……rpn},这样的操作就称为排序。
内排序和外排序
根据在排序过程中待排序的记录是否全部被放置在内存中,将其分为:内排序和外排序。
内排序是在排序的整个过程中,待排序的所有记录全部被放置在内存中,外排序是由于需要排序的记录太多,不能同时放置咋子内存中,整个排序过程需要在内外存之间多次交换数据才能进行,
对于内排序来说,排序算法的性能主要受以下3个方面影响:
(1)时间性能,排序是数据处理中经常执行的操作,因此排序算法的时间性能是衡量其好坏的最重要的标志,内排序中主要进行比较和移动这两种操作。高效率的算法应该尽可能少的进行比较和移动。
(2)辅助空间,指的是除了存放待排序数据所占用的存储空间外,执行算法所需要的其他存储空间。
(3)算法的复杂性。
冒泡排序
冒泡排序(Bubble Sort)是一种交换排序,基本思想是两两比较相邻的元素,如果他们的顺序错误就把他们交换过来,直到排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端
冒泡排序算法的过程如下:
(1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
(3)针对所有的元素重复以上的步骤,除了最后一个。
(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
简单实现
void sort(int[] a) { for(int i=0;i<a.length;i++) { for(int j=0;j<a.length-i-1;j++) { if(a[j]>a[j+1]) { int temp=a[j]; a[j]=a[j+1]; a[j+1]=temp; } } } }
这应该是最简单的排序代码了,不过这个代码效率是非常低下的,所以我们需要进行改进。
冒泡排序优化
试想一下,有这么一个数组{2,1,3,4,5,6,7,8},也就是说,除了第一和第二个元素需要交换,其他的已经是正常的顺序了 ,如果我们用上面的算法,毫无疑问它会将每个循环再执行一次,这就耗费了大量的时间,所以我们可以设置一个标志位,当没有任何数据交换时说明已经有序,不需要进行后面的循环操作。
void bestsort(int[] a) { boolean flag=true; for(int i=0;i<a.length&&flag;i++) { flag=false; for(int j=0;j<a.length-i-1;j++) { if(a[j]>a[j+1]) { int temp=a[j]; a[j]=a[j+1]; a[j+1]=temp; flag=true; } } } }
冒泡排序时间复杂度分析
当最好的情况,也就是排序的数组本身是有序的,那么我们需要比较一轮,也就是n-1次,没有数据交换时间复杂度为O(n),最坏的情况,也就是排序的数组为逆序时,此时需要比较n-1+n-2+……2+1次,也就是n(n-1)/2,并且还需要移动,此时时间复杂度为O(n^2).这时候我们就知道了冒泡排序是一种效率多么低下的算法,尽管有很多人对它进行各种各样的优化,但是排序的特性在这里,性能依然大大相差于其他算法。
选择排序
选择排序的思想是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
简单选择排序
public static void selectsort(int[] arr) { for (int i=0;i<arr.length;i++) { int minindex=i; for(int j=i+1;j<arr.length;j++) { if(arr[minindex]>arr[j]) { minindex=j; } } if(i!=minindex) { int temp=arr[i]; arr[i]=arr[minindex]; arr[minindex]=temp; } } }
简单选择排序时间复杂度分析
从算法上来看,选择排序交换移动次数相当少,分析时间复杂度,无论是最好还是最坏情况,比较次数都是一样的多,为n(n-1)/2次,而交换次数最好情况为0,最坏情况逆序为n-1次,因此总的来看,选择排序的时间复杂度为O(n^2),虽然说和冒泡排序同为O(n^2),但是选择排序的性能还是要优于冒泡排序。
插入排序
插入排序的思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数加一的有序表。
void insertSort(int[] arr) { for(int i=1;i<arr.length;i++) { for(int j=i;j>0;j--) { if(arr[j]<arr[j-1]) { int temp=arr[j]; arr[j]=arr[j-1]; arr[j-1]=temp; }else{ break; } } } }
插入排序时间复杂度分析
当最好的情况,也就是要排序的表本身就有序时,那么只需要比较n-1次,此时时间复杂度为O(n),当最坏情况发生时,即待排序的数组为逆序,此时需要比较1+2+3+。。。(n-1)次,时间复杂度为O(n^2),但是同样为O(n^2),插入排序的性能要比冒泡和选择排序性能要好。
希尔排序
希尔排序是D.L.Shell发明的一种排序算法,在这之前的排序算法的时间复杂度基本都是O(n^2),希尔排序是突破这个时间复杂度的第一批算法之一,也被称为缩减增量排序。
基本思想是先将整个待排序的记录序列分割成为若干子序列(由相隔某个“增量”的元素组成)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,例如{2,1,3,6,4,7,5,8,9}就可以称为基本有序,但{1,5,9,3,7,8,2,4,6}这样的9在第三位,2很靠后,就谈不上基本有序。
我们来举一个例子来更好的理解一下希尔排序。
(1)我们有一个数组为{49,38,65,97,26,13,27,49,55,4},长度为10,我们设增量为数组长度/2,所以这里增量为5
(2) 然后按照增量将整个元素序列分为子序列,这里分为{49,13},{38,27},{65,49},{97,55},{26,4},然后对其进行直接排序为{13,49},{27,38},{49,65},{55,97},{4,26},第一次排序后即为13 ,27, 49 ,55 , 4, 49,38,65,97 ,26
(3)然后我们缩减增量,缩减规律为当前增量/2,即5/2=2,然后再分为子序列{13,49,4,38,97},{27,55,49,65,26},然后继续直接插入排序,以此类推
(4)当增量缩减到0时,则排序完成得到数组。
代码实现
void shellSort(int[] arr) { //确定增量,这里使用(数组长度/2),并且每次/2 for(int gap=arr.length/2;gap>0;gap/=2) { //分组进行交换 for(int i=gap;i<arr.length;i++) { for(int j=i-gap;j>=0&&arr[j]>arr[j+gap];j-=gap) { int temp=arr[j]; arr[j]=arr[j+gap]; arr[j+gap]=temp; } } } }
通过我们的分析,大家应该明白,希尔排序的关键就是增量,将相隔某个增量的数据组成一个子序列,形成跳跃式的移动,使得移动次数变少,效率变高。这里的增量的选取非常关键,可究竟选取什么增量才是最好?目前还是一个数学难题,到现在位置还没有找到一种最好的增量,不过大量的研究表明,当增量为 dlta[k]=2^(t-k+1)-1(0<=k<=t<=[log2(n+1)])时,可以有很不错的效率,时间复杂度为O(n^(3/2)),效率相比前面几种有了大大的提高,不过因为是跳跃式移动,希尔排序并不是一种稳定的排序算法。
堆排序
我们先来了解一下堆这种数据结构,堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。下图左为大顶堆,右为小顶堆
如果给结点按照层序遍历方式编号,则结点之间满足如下关系
堆排序就是利用堆进行排序的方法。基本思想是,将待排序的序列构造成一个大顶堆(或小顶堆),此时整个序列的最大值就是根结点,将其移除输出,然后将剩下的元素再构成一个大顶堆,继续输出最大的元素,反复执行,便能得到一个有序序列。
这里我们就需要解决两个最关键的问题
- 1.如何将n个待排序元素构建成堆?
- 2.输出堆顶元素后,如何调整剩余元素使其成为一个新的堆?
我们先来看第一个问题,我们曾经学习过一个结论,对于完全二叉树而言,第n个元素的双亲节点是n/2,也就是说最后一个结点n是n/2的子树,然后我们就从n/2结点开始排序,使其子树成为堆,然后从n/2以此向前对每一个有子树的根结点进行排序,使其成为堆,直到根结点。
然后我们来看第二个问题,如果有n个元素的堆,输出堆顶元素后,我们将堆底元素放在堆顶,然后再与其左右子树进行比较交换构建堆,这样到比较完排序过程也就完成了。
代码实现
public class HeapSort { public static void main(String[] args) { int[] sort = new int[] { 1, 0, 10, 20, 3, 5, 6, 4, 9, 8, 12, 17, 34, 11 }; heapSort(sort); for(int i:sort) { System.out.print(i+" "); } } //堆排序 private static void heapSort(int[] data) { //先将当前数组转换为最大堆 buildMaxHeapify(data); // 末尾与头交换,然后将剩下的元素构建最大堆 for (int i = data.length - 1; i > 0; i--) { int temp = data[0]; data[0] = data[i]; data[i] = temp; maxHeapify(data, i, 0); } } //构建最大堆 private static void buildMaxHeapify(int[] data) { //从最后一个具有子树的结点开始构建 for (int i = data.length/2; i >= 0; i--) { maxHeapify(data, data.length, i); } } /** *创建最大堆 * data为数组,heapsize为数组大小,index为当前根结点 **/ private static void maxHeapify(int[] data, int heapSize, int index) { // 获取当前结点的左右孩子结点 int left =index*2; int right = index*2+1; //与左右结点判断,获取最大的 int largest = index; if (left < heapSize && data[index] < data[left]) { largest = left; } if (right < heapSize && data[largest] < data[right]) { largest = right; } // 得到最大值后可能需要交换,如果交换了,其子节点可能就不是最大堆了,所以还需要递归调整其子结点 if (largest != index) { int temp = data[index]; data[index] = data[largest]; data[largest] = temp; maxHeapify(data, heapSize, largest); } }}
堆排序的运行时间主要用在初始化构建堆和重建堆的反复筛选上,堆排序的时间复杂度为O(nlogn),这在性能上显然要远远好于冒泡、简单选择、直接插入的O(n^2)的时间复杂度。有兴趣的可以去看一下堆排序的数学计算时间复杂度的过程
归并排序
归并排序就是利用归并的思想实现排序。基本思想是假设初始序列有n个记录,可以将其看成n个子序列,然后两两归并,得到n/2个有序子序列,然后继续两两归并,直到最后得到一个长度为n的有序序列。这种排序方法称为2路归并排序。
对于给定一个无序的数组,我们需要先将其拆分为一个个有序子序列,然后再进行合并。
我们先来看看如何将两个有序序列进行合并,这个非常简单,只要以此比较两个序列中的数,谁小就取谁,如果有一个序列空了,那么就依次把剩下序列中数放入即可。那么如何使一个无序序列分解为有序子序列呢,我们可以将当前序列不断的进行分解,当分出来的子序列只有一个数据时,可以认为这个子序列已经达到了有序,然后再合并相邻的二个子序列就可以了
代码实现
public class MergeSort { public static void main(String[] args) { int []array= {9,8,7,6,5,4,23,2,1,0}; mergeSort(array); for (int i = 0; i < array.length; ++i) { System.out.print(array[i] + " "); } } public static void mergeSort(int[] array) { //创建一个辅助数组来方便我们进行合并 int[] temp=new int[array.length]; //将整个数组分解 mergesort(array,0,array.length-1,temp); } //将整个无序数组分解为一个个的单元素,然后合并,参数为无序数组、数组的起始位置、数组的结束位置、辅助数组 static void mergesort(int a[], int first, int last, int temp[]) { //如果起始位置小于结束位置,说明未完全分解,则继续递归 if (first < last) { //获取中间位置,进行分解 int mid = (first + last) / 2; //对左边数组进行分解 mergesort(a, first, mid, temp); //对右边数组进行分解 mergesort(a, mid + 1, last, temp); //分解完毕后,进行合并 mergearray(a, first, mid+1, last, temp); //再将二个有序数列合并 } } //firstindex为第一个数组起始位置,secondindex为第二个数组起始位置,last为第二个数组结束位置 static void mergearray(int a[], int firstindex, int secondindex, int last, int temp[]) { //获取第一个数组的结束位置 int firstend=secondindex-1; int tmppos=firstindex; //获取当前合并的元素个数 int numbers=last-firstindex+1; //进行比较 while (firstindex <= firstend && secondindex <= last) { if (a[firstindex] <= a[secondindex]) temp[tmppos++] = a[firstindex++]; else temp[tmppos++] = a[secondindex++]; } //如果某个数组为空了,则将另一个数组剩下的元素依次放入数组 while (firstindex <= firstend) temp[tmppos++] = a[firstindex++]; while (secondindex <= last) temp[tmppos++] = a[secondindex++]; //将我们的辅助数组复制到原数组中,要从后向前复制 for (int i = 0; i < numbers; i++,last--) a[last] = temp[last]; } }
归并排序时间复杂度分析
归并排序的时间复杂度为O(nlogn),因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
快速排序
快速排序的基本思想是:选择一个基准元素(称为枢纽元),通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比基准元素小,另一部分元素都比基准元素大,这时候枢纽元元素的位置就已经确定了,然后再分别对这两部分记录继续进行排序,直到左右两部分只有一个数时结束,以达到整个序列有序的目的。
public class QuickSort { public static void main(String[] args) { int [] arr= {74,200,200,74,57,1023,1,85,32,57,99}; //调用快速排序 quickSort(arr); for(int a:arr) { System.out.print(a+" "); } } public static void quickSort(int[] array) { quickSort(array,0,array.length-1); } public static void quickSort(int[] array,int low,int high) { int pivot; if(low<high) { //得到枢纽元的位置,这时候枢纽元已经固定了在数组中的位置 pivot=partition(array,low,high); //对左侧子序列进行快排 quickSort(array,low,pivot-1); //对右侧子序列进行快排 quickSort(array,pivot+1,high); } } //选取枢纽元,然后把它放入固定的位置,使其左侧元素都小于它,右侧元素都大于它 public static int partition(int[] array,int low, int high) { //选取第一个元素为枢纽元 int pivotkey=array[low]; while(low<high) { //当右边元素小于枢纽元时跳出循环,与左边元素交换位置 while(low<high&&array[high]>=pivotkey) { high--; } swap(array,low,high); //当左边元素大于枢纽元时跳出循环,与右边元素交换位置 while(low<high&&array[low]<=pivotkey) { low++; } swap(array,low,high); } //返回当前枢纽元确定的位置 return low; } public static void swap(int[] array,int low,int high) { int temp=array[low]; array[low]=array[high]; array[high]=temp; }}
枢纽元优化
我们前面的枢纽元直接选取了第一个元素,这是一种非常蠢的做法,如果我们的数组是反序的,那么就会产生一个特别差的分割效果,所有元素都被分割到一侧,时间效率是二次的,极其差劲,所以我们需要改变一下。有人提出了一种方法就是随机选取枢纽元,但是也不是最好的选择,因为随机数生成也花费不少的时间性能。于是就有了我们要使用的三数取中法。
三数取中法即取三个关键字先进行排序,将中间数作为枢纽元。一般是选左端、右端和中间三个数。这样这个中间数一定不会是最小或者最大的数。
我们只需要在取枢纽元的前面加上如下代码 public static int partition(int[] array,int low, int high) { //交换之后,array[low]位置上就是我们的中间值 int pivotkey; int mid=low+(high-low)/2; if(array[low]>array[high]) { swap(array,low,high); } if(array[mid]>array[high] ) { swap(array,high,mid); } if(array[mid]>array[low] ) { swap(array,mid,low); } pivotkey=array[low]; while(low<high) { while(low<high&&array[high]>=pivotkey) { high--; } swap(array,low,high); while(low<high&&array[low]<=pivotkey) { low++; } swap(array,low,high); } return low; }
小数组优化
对于很小的数组,快速排序的效率是不如插入排序的。所以我们增加一个判断,当high-low不大于某个常数,有研究说是7比较合适,就使用插入排序。
public static void quickSort(int[] array,int low,int high) { if(high-low>7) { int pivot; pivot=partition(array,low,high); quickSort(array,low,pivot-1); quickSort(array,pivot+1,high); }else { InsertSort.insertSort(array); } }
当然快速排序还有很多种的更加细致的优化,有兴趣可以去了解一下。
- JAVA数据结构和算法:第八章(排序)
- 数据结构 第八章 排序
- Java 数据结构和算法 排序
- Java数据结构和算法---排序
- Java数据结构和算法-简单排序(1-冒泡排序)
- Java数据结构和算法-简单排序(2-选择排序)
- Java数据结构和算法-简单排序(3-插入排序)
- 算法和数据结构-排序-插入排序(Java)
- 算法和数据结构-排序-希尔排序(Java)
- Java数据结构和算法-高级排序(1-希尔排序)
- 《数据结构》 第八章 排序 笔记
- 《Java数据结构和算法》第二版 Robert lafore 编程作业 第八章
- 《Java数据结构和算法》第二版 Robert lafore 编程作业 第八章
- java数据结构和算法 第3章 简单排序
- java数据结构和算法---基本查找排序
- java数据结构和算法-1,简单排序
- java数据结构和算法-3,希尔排序
- java数据结构和算法-3,快速排序
- 集训考试2
- 分布式理论基础-选举、多数派、租约
- oracle密码参数
- 9.9 极其简单的最短路问题 2721
- 解释一下关系数据库的第一第二第三范式?
- JAVA数据结构和算法:第八章(排序)
- C#梳理【多态】
- Android图文混排实现方式详解
- 594. Longest Harmonious Subsequence
- LeetCode Task Scheduler
- 安装Nginx依赖的包到CentOS 6.5
- 机器学习-梯度下降VI(学习率)
- Adapter基础讲解-SimpleCursorAdapter使用示例
- 获取输入框的值,计算出的支付费用