1.排序的概念
排序就是使得一串记录,按照其中的某个或者某些关键字的大小,递增或者递减的排列起来的操作。
**稳定性:**假如在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对序列不变,仍然在原序列中,即r[i]=r[j],且r[i]在r[j]之前,而在排序后之后两者顺序不变则称这种排序算法稳定。
内部排序:数据元素全部放在内存中排序
外部排序:数据元素过多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2.常见排序算法实现
2.1插入排序
2.1.1直接插入排序
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到 一个新的有序序列
java
public static void insertSort(int[] array){
for (int i = 1; i < array.length; i++) {
int tmp=array[i];
int j = i-1;
for (; j >0 ; j--) {
if(array[j]>tmp){
array[j+1]=array[j];
}else {
break;
}
array[j+1]=tmp;
}
}
}
代码思路:定义两个下标,i和j,i从数组第二个元素开始,j=i-1。将当前i的元素寄存tmp,与前面的元素进行比较,若数据元素大于tmp,将前序元素覆盖当前元素,并且继续往前遍历;若数据元素小于tmp则将当前空出的元素的地方用tmp补充。若j一直遍历到小于0,遍历完了那么也将空缺位置用tmp填充。
2.1.2希尔排序
先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1****时,所有记录在统一组内排好序。
java
public static void shellSort(int[] array){
int gap= array.length;;
while(gap>1){
gap/=2;
shell(array,gap);
}
}
private static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int tmp=array[i];
int j = i-gap;
for (; j >0 ; j-=gap) {
if(array[j]>tmp){
array[j+gap]=array[j];
}else {
break;
}
array[j+gap]=tmp;
}
}
}
代码思路:直接排序可以看作gap==1的时候的情况,所以可以将直接排序的代码进行修改。j下标每次移动值的区间应该为gap。gap每次排序后不断缩小,可以让每次除以2.
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算。
2.2选择排序
2.2.1直接选择排序
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
java
public static void selectSort(int[] array){
for (int i = 0; i < array.length; i++) {
int minIndex=i;
for (int j = i+1; j < array.length ; j++) {
if(array[j]<array[minIndex]) {
minIndex = j;
}
}
swap(array,minIndex,i);
}
}
private static void swap(int[] array, int minIndex, int i) {
int tmp=array[i];
array[i]=array[minIndex];
array[minIndex]=tmp;
}
代码思路:假定当前下标的值为最小值,将这个值与数组后面的值进行大小比较若发现还有更小的值,将两个值进行交换。
我们还可以进行优化一下,同时操作最小值和最大值。
java
private static void swap(int[] array, int minIndex, int i) {
int tmp=array[i];
array[i]=array[minIndex];
array[minIndex]=tmp;
}
public static void selectSort2(int[] array){
int left=0;
int right= array.length-1;
while(left<right){
int minIndex=left;
int maxIndex=left;
for (int i = left+1; i <=right ; i++) {
if(array[i]<array[minIndex]){
minIndex=i;
}
if(array[i]>array[maxIndex]){
maxIndex=i;
}
}
swap(array,minIndex,left);
if(maxIndex==left){
maxIndex=minIndex;
}
swap(array,maxIndex,right);
left++;
right--;
}
代码思路:每次将最小值放在数组的最左边最大值放在最右边,需要注意的是,交换的时候minIndex和left先进行了交换,若此时maxIndex==left,那么minIndex才是原先maxIndex坐标。
2.2.2堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
java
public static void heapSort(int[] array) {
createTree(array);
int end = array.length - 1;
while (end > 0) {
swap(array, 0, end);
shiftdown(array, 0, end);
end--;
}
}
private static void createTree(int[] array) {
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftdown(array, parent, array.length);
}
}
private static void shiftdown(int[] array, int parent, int end) {
int child = 2 * parent + 1;
while (child < end) {
if (child + 1 < end && array[child] < array[parent]) {
child++;
}
if (array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
}else {
break;
}
}
}
2.3交换排序
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.1冒泡排序
java
public static void bubbleSort(int[] array){
for (int i = 0; i < array.length-1; i++) {
boolean flg=false;
for (int j = 0; j < array.length-1-i ; j++) {
if(array[j]>array[j+1]){
swap(array,j,j+1);
flg=true;
}
}
if(!flg){
return;
}
}
}
2.3.2快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.3.2.1Hoare版
java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array, int start, int end) {
if(start>end){
return;
}
int pivot=partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static int partition(int[] array, int left, int right) {
int key=array[left];
int i=left;
while(left<right){
while(left<right&&array[right]>=key){
right--;//right去寻找比key小的值用于交换,比key大则跳过
}
while(left<right&&array[left]<=key){
left++;//left去寻找比key大的值用于交换,比key小则跳过
}
swap(array,left,right);
}
swap(array,left,i);//相遇后当前位置为基准,将当前值与key进行对换。保证基准的左边全部小于基准,右边全部大于基准。
return left;//返回基准下标
}
}
循环内部一定要取等号,以防当前置与key相同的时候,陷入交换的死循环。之所以先走right是为了确保基准的左边都比基准要小。
2.3.2.2挖坑法
java
private static int partition(int[] array, int left, int right) {
int key=array[left];//取基准值并挖坑
while(left<right){
while(left<right&&array[right]>=key){
right--;
}
array[left]=array[right];//若当前值比key小则填坑
while(left<right&&array[left]<=key){
left++;
}
array[left]=array[right];//若当前值比key大则填坑
}
array[left]=key;//相遇
return left;
}
2.3.2.3前后指针法
java
private static int partition(int[] array, int left, int right) {
int prev = left ;
int cur = left+1;
while (cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
2.3.3快速排序的优化
1.三个数取中法选择key,使得二叉树的深度变小
2.递归到小的子区间的时候,可以考虑使用插入排序
java
private static void quick(int[] array, int start, int end) {
if(start>end){
return;
}
if(end-start+1<=7){
insertSortRange(array,start,end);
return;
}
int index=midOfThree(array,start,end);
int pivot=partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static int midOfThree(int[] array, int left, int right) {
int mid=(left+right)/2;
if(array[left]<array[right]){
if(array[mid]<array[left]) {
return left;
} else if (array[mid]>array[right]){
return right;
}else {
return mid;
}
}else {
if(array[mid]>array[left]){
return left;
} else if (array[mid]<array[right]) {
return right;
}else {return mid;}
}
}
private static void insertSortRange(int[] array, int start, int end) {
for (int i = start+1; i <=end ; i++) {
int tmp=array[i];
int j = i-1;
for (; j >=start ; j--) {
if(array[j]>tmp){
array[j+1]=array[j];
}else {
break;
}
array[j+1]=tmp;
}
}
}
2.3.4快速排序非递归
java
public static void quickSortNor(int[] array){
Stack<Integer> stack=new Stack<>();
int left=0;
int right=array.length-1;
int piovt=partition(array,left,right);
if(piovt-1>left){
//只有当左边不止一个元素的时候
stack.push(left);
stack.push(piovt-1);
}
if(piovt+1<right){
stack.push(piovt+1);
stack.push(right);
//右边不止一个元素
}
while(!stack.isEmpty()){
right=stack.pop();//后进先出
left=stack.pop();
piovt=partition(array,left,right);
if(piovt-1>left){
stack.push(left);
stack.push(piovt-1);
}
if(piovt+1<right){
stack.push(piovt+1);
stack.push(right);
}
}
}
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2.4归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
先分解后合并
java
public static void mergeSort(int[] array){
mergeSortDe(array,0,array.length-1);
}
private static void mergeSortDe(int[] array,int left,int right) {
if(left>=right) return;
int mid=(left+right)/2;
mergeSortDe(array,left,mid);
mergeSortDe(array,mid+1,right);
merge(array,left,right,mid);
}
private static void merge(int[] array, int left, int right, int mid) {
int s1=left;
int s2=mid+1;
int[] tmpArr=new int[right-left+1];
int k=0;
while(s1<mid&&s2<=right){
if(array[s2]<=array[s1]){
tmpArr[k++]=array[s2++];
}else {
tmpArr[k+=]=array[s1++];
}
}
while(s1<=mid){
tmpArr[k++]=array[s1++];
}
while (s2<=right){
tmpArr[k++]=array[s2++];
}
for (int i = 0; i < tmpArr.length; i++) {
array[i+left] = tmpArr[i];
}
}
2.5海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
- 先把文件切分成 200 份,每个 512 M
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
- 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
3.排序算法复杂度及稳定性分析
|----------|----------------|----------------|----------------|-------------------|---------|
| 排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
| 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
| 插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 希尔排序 | O(n) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
| 堆排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(1) | 不稳定 |
| 快速排序 | O(n * log(n)) | O(n * log(n)) | O(n^2) | O(log(n)) ~ O(n) | 不稳定 |
| 归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(n) | 稳定 |
4.其他排序
4.1计数排序
- 统计相同元素出现的次数
- 根据统计的结果将序列回收到原来的序列中
java
public static void countSort(int[] array){
int minVal=array[0];
int maxVal=array[0];
//求当前数组的最大值和最小值
for (int i = 1; i <array.length ; i++) {
if(array[i]<minVal){
minVal=array[i];
}
if(array[i]>maxVal){
maxVal=array[i];
}
}
//根据最大值和最小值来确定数组的大小
int[] count=new int[maxVal-maxVal+1];
//根据原来的数组进行计数
for (int i = 0; i < array.length; i++) {
count[array[i]-minVal]++;
}
//根据计数数组打印
int index=0;
for (int i = 0; i < array.length; i++) {
while(count[i]>0){
array[index]=i+minVal;
index++;
count[i]--;
}
}
}
4.2基数排序
基数排序(Radix Sort)是一种非比较型的排序算法,它通过逐位比较元素的每一位(从最低位到最高位)来实现排序。基数排序的核心思想是将整数按位数切割成不同的数字,然后按每个位数分别进行排序。基数排序的时间复杂度为 O(n * k),其中 n 是列表长度,k 是最大数字的位数。
算法步骤:
-
确定最大位数:找到列表中最大数字的位数,确定需要排序的轮数。
-
按位排序:从最低位开始,依次对每一位进行排序(通常使用计数排序或桶排序作为子排序算法)。
-
合并结果:每一轮排序后,更新列表的顺序,直到所有位数排序完成。
4.3桶排序
一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。
桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。