排序定义
定义 | 描述 |
---|---|
排序 | 假设n个记录的序列为 { r 1 , r 2 , . . . , r n } \{r1,r2,...,rn\} {r1,r2,...,rn},其相应的关键字分别为 { k 1 , k 2 , . . . , k n } \{k1,k2,...,kn\} {k1,k2,...,kn},需确定1,2,…,n的一种排列p1,p2,…,pn,使其相应的关键字满足 k p 1 ≤ k p 2 ≤ . . . ≤ k p n k_{p1}\leq k_{p2}\leq ...\leq k_{pn} kp1≤kp2≤...≤kpn 关系,即使得序列成为一个按关键字有序的序列 { r p 1 , r p 2 , . . . , r p n } \{r_{p1},r_{p2},...,r_{pn}\} {rp1,rp2,...,rpn},这样的操作称为排序。 |
排序的稳定性 | 假设 k i = k j ( 1 ≤ i ≤ n , 1 ≤ j ≤ n , i ≠ j ) k_i=k_j(1\leq i \leq n,1\leq j \leq n ,i\neq j) ki=kj(1≤i≤n,1≤j≤n,i̸=j),且在排序前的序列中 r i r_i ri领先于 r j r_j rj(即 i < j i<j i<j)。如果排序后 r i r_i ri仍然领先于 r j r_j rj,则称所用的排序方法是稳定的;反之,如果排序后 r j r_j rj领先于 r i r_i ri,则排序是不稳定的。 |
内排序 | 在排序整个过程中,待排序的所有记录全部放置在内存中。排序算法的性能主要受3个方面影响:1、时间性能;2、辅助空间;3、算法的复杂性。主要分为:插入排序、交换排序、选择排序和归并排序。简单算法包括冒泡排序、简单选择排序、直接插入排序;改进算法包括希尔排序、堆排序、归并排序、快速排序。 |
外排序 | 由于排序的记录个数太多,不能同时放在内存,整个排序过程需要在内外存之间多次交换数据才能进行。 |
简单排序
排序方法 | 时间复杂度 | 描述 |
---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | 一种交换排序,基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。 |
简单选择排序 | O ( n 2 ) O(n^2) O(n2) | 通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i ( 1 ≤ i ≤ n ) (1\leq i\leq n ) (1≤i≤n)个记录交换。 |
直接插入排序 | O ( n 2 ) O(n^2) O(n2) | 将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。 |
冒泡排序
初级版:
- 将元素一个个从前到后比较,较小的放到前面:
比较第i个元素array[i]和它后面的元素array[j],如果array[i]>array[j],则交换比较第i个元素array[i]和它后面的元素array[j],如果array[i]>array[j],则交换; - 循环至i=length-1。
//不是标准的冒泡排序算法
//交换排序,最小的直接交换到最前面一个
public static void bubbleSort0(int[] array){
int i,j;
for (i=0;i<array.length;i++){
for (j=i+1;j<array.length;j++){
if (array[i]>array[j]){
swap(array,i,j);
}
}
}
}
标准版:
- 将元素和相邻的元素比较,较小的元素放在下标较小的位置:
从j=length-1开始,比较array[j-1]和array[j]的大小,选择交换与否使得较小的元素放到array[j-1]中; - 循环至i=length-1。
//标准的冒泡排序算法
public static void bubbleSort1(int[] array){
int i,j;
for (i=0;i<array.length;i++){
for (j=array.length-1;j>i;j--){
if (array[j-1]>array[j])
swap(array,j,j-1);
}
}
}
改进版:
- 改进的地方在于:
假如已经序列有序了,停止排序。有序时没有发生交换操作,所以设置一个发生交换的标志位,没有发生交换时整个排序停止。 - 其他步骤和标准版一致。
//改进的冒泡排序:
//当没有任何数据交换时,表明数据已经有序,无须后续的判断
public static void bubbleSort2(int[] array){
int i,j;
boolean hasSwaped=true;
for (i=0;i<array.length&&hasSwaped;i++){
hasSwaped=false;
for (j=array.length-1;j>i;j--){
if (array[j-1]>array[j]) {
swap(array, j, j - 1);
hasSwaped=true;
}
}
}
}
简单选择排序
优点在于交换次数少,节省相应的时间,性能优于冒泡排序。
排序过程:
- 在length-i个元素中找到最小的那个元素的下标min;
- 如果i和min不相等,将第i个元素和第min个元素交换。
- 循环直到i=length-1;
//简单选择排序
//通过n-i次关键字间的比较,从n-1+1个记录中选出关键字最小的记录
public static void selectSort(int[] array){
int i,j,min;
for (i=0;i<array.length;i++){
//找到最小的下标,如果不是i,则和i交换
min=i;
for (j=i+1;j<array.length;j++){
if (array[min]>array[j]){
min=j;
}
}
if (i!=min){
swap(array,i,min);
}
}
}
直接插入排序
在简单排序算法中最佳的。
排序步骤:
- 将数组分成前面一个有序部分和后面一个无序部分;
- 把无序数组的头一个元素拿出来作为insertElement,将有序数组中大于insertElement的元素往后挪一个位置,插入insertElement;
- 循环至i=length-1。
//直接插入排序
//分成了两部分,前一部分有序,后一部分无序,从无序插入到无序表。
public static void insertSort(int[] array){
int i,j,insertElement;
//前i个元素为有序的
for (i=1;i<array.length;i++){
if (array[i]<array[i-1]){
insertElement=array[i];
//从有序数组的最后一个元素开始,把大于insertElement的元素往后挪一个位置,将insertElement插入
for (j=i-1;j>=0&&array[j]>insertElement;j--){
array[j+1]=array[j];
}
array[j+1]=insertElement;
}
}
}
改进排序
排序方法 | 复杂度 | 描述 |
---|---|---|
希尔排序 | O ( n 3 / 2 ) O(n^{3/2}) O(n3/2) | 直接插入排序的改进。将整个序列分割成若干个子序列,然后在这些子序列内分别进行直接插入排序,当整个序列基本有序时,对全体记录进行一次直接插入排序。 |
堆排序 | O ( n l o g n ) O(nlog{n}) O(nlogn) | 简单选择排序的改进。堆排序是利用堆(假设采用大顶堆)进行排序的方法。将待排序的序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根结点;将它移走(其实是和数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。反复执行,便能得到一个有序序列。 |
归并排序 | O ( n l o g n ) O(nlog{n}) O(nlogn) | 假设初始序列含n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]个长度为2或者1的有序子序列;再两两归并,…,重复直至得到一个长度为n的有序序列为止,称为2路归并排序。 |
快速排序 | O ( n l o g n ) O(nlog{n}) O(nlogn) | 通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可对这两部分记录继续进行排序,以达到整个序列有序的目的。 |
希尔排序
将相距某个“增量”的记录组成一个子序列。保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
由于是跳跃式的移动,希尔怕徐不是一个稳定的排序算法。
排序过程:
- 初始化增量increment=数组长度;
- 循环过程:
increment=increment/3+1,与首元素相隔increment的元素相比较,后面元素比较小时说明需要插入操作,插入操作仅发生在以i-increment为尾,间隔为increment的子序列中,{…,i-2increment,i-increment}是有序的,array[i]在这个序列进行插入。 - 循环直到increment=1。
/**
* 希尔排序:
* 1.将整个序列分割成若干个子序列,然后在这些子序列内分别进行直接插入排序;
* 2.当整个序列基本有序时,对全体记录进行一次直接插入排序。
* @param array 待排序的数组
*/
public static void shellSort(int[] array){
int increment=array.length;
int insertElement;
while (increment>1){
increment=increment/3+1;
for (int i=increment;i<array.length;i++){
if (array[i]<array[i-increment]){
insertElement=array[i];
int j;
for (j=i-increment;j>=0&&insertElement<array[j];j-=increment){
array[j+increment]=array[j];
}
array[j+increment]=insertElement;
}
}
}
}
堆排序
堆是具有下列性质的完全二叉树:
- 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
排序过程:
- 将待排序序列构成一个大顶堆;
- 逐步将每个最大值的根结点与末尾元素交换,并再调整其成为大顶堆。
使某个结点满足大顶堆的要求:
- 对于初始化一个大顶堆,注意到在heapSort中从length/2-1这个结点开始往根结点实现大顶堆, length/2-1是一个叶子结点的父节点,对于这样的结点,次函数很明显会满足实现;再逐步往根结点推进,直到整个数组满足一个大顶堆;
- 对于交换结点后重新调整为大顶堆, 注意,不同于重新构建一个大顶堆,因为除了nodeToAdjust,其他结点都是满足大顶堆要求的。所以,看孩子结点有没有比它大的,如果没有,退出循环,满足大顶堆要求;如果有,把孩子结点的值赋给该结点,并且孩子结点成为新的需要调整的结点。
/**
* 堆排序:
* 1.构建一个大顶堆;
* 2.逐步将每个最大值的根结点与末尾元素交换,并再调整其成为大顶堆。
* @param array
*/
public static void heapSort(int[] array){
int i;
for (i=array.length/2-1;i>=0;i--){
heapAdjust(array,i,array.length);
}
for (i=array.length-1;i>0;i--){
swap(array,0,i);
heapAdjust(array,0,i-1);
}
}
private static void heapAdjust(int[] array, int nodeToAdjust,int maxIndex){
int temp=array[nodeToAdjust];
// 对于初始化一个大顶堆,
// 注意到在heapSort中从length/2-1这个结点开始往根结点实现大顶堆,
// length/2-1是一个叶子结点的父节点,对于这样的结点,次函数很明显会满足实现;
// 在逐步往根结点推进,直到整个数组满足一个大顶堆。
// 对于交换结点后重新调整为大顶堆:
// 注意,不同于重新构建一个大顶堆,因为除了nodeToAdjust,其他结点都是满足大顶堆要求的。
// 看孩子结点有没有比它大的,如果没有,退出循环;如果有,把孩子结点的值赋给该结点,并且孩子结点称为新的需要调整的结点。
for (int i=2*nodeToAdjust+1;i<=maxIndex;i=2*i+1){
if (i<maxIndex&&array[i]<array[i+1]){
i++;
}
if (temp>=array[i]){
break;
}
array[nodeToAdjust]=array[i];
nodeToAdjust=i;
}
array[nodeToAdjust]=temp;
}
归并排序
归并排序的复杂度为
O
(
n
l
o
g
n
)
O(nlog{n})
O(nlogn)。
排序过程:
- 左边归并排序,使得左子序列有序;右边归并排序,使得右子序列有序;
- 合并两个子序列:
首先创建一个left至right大小的数组用来暂存合并数组(或者直接申请一块与原数组大小相等的内存temp,每次都传入这个数组,避免频繁申请内存);
两个变量i、j分别从要合并的两个子序列的起点开始,比较指针i、j指向的两个子序列对应元素的大小,将小的存进该数组,直到有一个子序列遍历完了(此时i、j中有一个到了子序列的右端点);
将另一个子序列剩余的元素直接装入合并数组后,再将合并数组搬移到原数组之中,完成整个合并过程。
/**
* 归并排序:
* 时间复杂度为O(nlogn)
*/
public static void merSort(int[] arr,int left,int right){
if(left<right){
int mid = (left+right)/2;
merSort(arr,left,mid);//左边归并排序,使得左子序列有序
merSort(arr,mid+1,right);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right);//合并两个子序列
}
}
private static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];//ps:也可以从开始就申请一个与原数组大小相同的数组,因为重复new数组会频繁申请内存
int i = left;
int j = mid+1;
int k = 0;
while(i<=mid&&j<=right){
if (arr[i] < arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[k++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[k++] = arr[j++];
}
//将temp中的元素全部拷贝到原数组中
for (int k2 = 0; k2 < temp.length; k2++) {
arr[k2 + left] = temp[k2];
}
}
快速排序
快速排序的复杂度为
O
(
n
l
o
g
n
)
O(nlog{n})
O(nlogn)。
排序过程:
- partition函数:选取当中的一个关键字,然后将它放到一个位置,使得它左边的值都比它小,右边的值比它大,这样的关键字称为枢轴(pivot);
partition操作的过程:
首先选取一个数为枢轴,然后将比枢轴记录小的数字交换到低端,将比枢轴记录大的数字交换到高端,最后返回枢轴的位置。 - 对低子表和高子表分别递归排序。
优化:
3. 优化中心枢轴,可以三数取中,取三个关键字先进行排序,将中间数作为枢轴,一般是取左端、中间和右端的三个数;
4. 优化不必要的交换,不用交换数据,暂存枢轴位置的值,直接替换数据,最后赋值给枢轴位置的值;
5. 数组小时采用直接插入排序;
6. 优化递归操作,将其中一次递归变成了while循环来实现。
/**
* 快速排序,时间复杂度O(nlogn),空间复杂度O(logn)
* 快速排序是一种不稳定的排序方法。
* 优化:
* 1.三数取中,优化选取枢轴
* 2.优化不必要的交换
* 3.小数组时,采用插入排序
* 4.递归转成迭代,缩短堆栈深度
* @param array
*/
public static void quickSort(int[] array){
qSort(array,0,array.length-1);
}
private static void qSort(int[] array,int low,int high){
//枢轴值
int pivot;
//3.小数组时采用插入排序
// if (low<high){
// pivot=partition(array,low,high);
// qSort(array,low,pivot-1);
// qSort(array,pivot+1,high);
// }
if (high-low>MAX_LENGTH_INSER_SORT){
pivot=partition(array,low,high);
qSort(array,low,pivot-1);
qSort(array,pivot+1,high);
}else
insertSort(array);
}
private static void qSort1(int[] array,int low,int high){
//枢轴值
int pivot;
if (high-low>MAX_LENGTH_INSER_SORT){
//4.递归转换成迭代,缩减堆栈深度
while (low<high) {
pivot = partition(array, low, high);
qSort1(array, low, pivot - 1);
low=pivot+1;
}
}else
insertSort(array);
}
private static int partition(int[] array,int low,int high) {
//选取的枢轴,是性能瓶颈
//1.三数取中:将三个关键字排序,将中间数作为枢轴
int middle=low+(high-low)/2;
if (array[low]>array[high])
swap(array,low,high);
if (array[middle]>array[high])
swap(array,high,middle);
if (array[middle]>array[low])
swap(array,middle,low);
int pre=array[low];
//2.优化不必要的交换
while (low < high){
//比枢轴记录小的交换到低端
while (low < high && array[high] >= pre)
high--;
// swap(array, low, high);
array[low]=array[high];
//比枢轴记录大的交换到高端
while (low < high && array[low] <= pre)
low++;
// swap(array, low, high);
array[high]=array[low];
}
array[low]=pre;
return low;
}