文章目录
1.排序分类
- 内部排序
- 数据记录在内存中进行排序
- 八大排序属于内部排序
- 外部排序
- 因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存
1.5.八大排序
- 插入排序–每一步将一个待排序的元素,按其排序码的大小,插入到前面已经排好序的一组元素的适当位置上去,直到元素全部插入为止
- 直接插入排序
- 希尔排序
- 选择排序
- 简单选择排序
- 堆排序
- 交换排序
- 冒泡排序
- 快速排序
- 归并排序
- 基数排序
2.插入排序
2.1.直接插入排序
-
基本思想:
-
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:首先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录开始与子序列逐个进行比较,找到插入位置插入,原来位置上的元素向后顺移逐个进行插入,直至整个序列有序为止。
-
如果子序列中遇到与插入元素相等的,置于该元素后面
-
要点:设立哨兵,作为临时存储和判断数组边界之用
-
-
时间复杂度:O(n^2)
-
使用场景:小规模数据或基本有序时
-
private static void rapidSort(int[] arr) { int length = arr.length; for (int i = 1; i < length; i++) { int x=arr[i]; //将插入元素设为哨兵 int j=i-1; while(j>=0&&x<arr[j]){ //哨兵依次与子序列元素比较 arr[j+1]=arr[j]; //子序列元素往后移一位 j--; } arr[j+1]=x; printAll(arr,i); //打印当前数组 } }
2.2希尔排序
-
基本思想:
- 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
-
算法过程:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 一般初始增量取序列长度一半,然后逐次减半
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。
- 仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
-
时间复杂度:平均O(n1.3),最坏O(n2)
-
排序过程:
-
private static void shellSort(int[] arr) { int length=arr.length; for (int gap=length/2;gap>0;gap/=2){ //进行分组,第一步的增量gap为数组长度一半,逐步减半 for (int i=gap;i<length;i++){ //对各个分组进行插入排序 insert(arr,gap,i); //将arr[i]插入到组内正确位置 } } } private static void insert(int[] arr, int gap, int i) { int inserted=arr[i]; int j; for (j=i-gap;j>=0&&inserted<arr[j];j-=gap){ //组内元素两两相隔gap arr[j+gap]=arr[j]; } arr[j+gap]=inserted; }
3.选择排序
3.1.简单选择排序
-
基本思想:
- 在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止
- 可加多一个记录最大元素的指针改为二元选择排序,改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可
-
时间复杂度:O(n^2)
-
private static void selectSort(int[] arr) { int length = arr.length; for (int i = 0; i < length; i++) { int minKey=getMinKey(arr,length,i); //获取其它剩余元素中最小元素的索引 if(i!=minKey){ //与其交换 int temp=arr[i]; arr[i]=arr[minKey]; arr[minKey]=temp; } printAll(arr,i+1); } } private static int getMinKey(int[] arr, int length, int i) { int k=i; for (int j=i;j<length;j++){ //遍历寻找最小索引 if(arr[j]<arr[k]){ k=j; } } return k; }
3.2.堆排序
-
基本思想:
- 堆排序是将数据看成是完全二叉树、根据完全二叉树的特性来进行排序的一种算法
- 完全二叉树:左边子节点位置 = 当前父节点的两倍 + 1,右边子节点位置 = 当前父节点的两倍 + 2,且所有非叶结点的值均不大于(或不小于)其子女的值
- 最大堆要求节点的元素都要不小于其孩子,最小堆要求节点元素都不大于其左右孩子
- 处于最大堆的根节点的元素一定是这个堆中的最大值
- 先将数据组成基本最大堆,接着把根节点元素与最后一个节点元素进行交换,然后除末尾元素外再进行形成堆,得出第二大元素与倒数第二个元素交换,以此进行,完成排序
- 堆排序是将数据看成是完全二叉树、根据完全二叉树的特性来进行排序的一种算法
-
建基本堆
-
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。
1)n 个结点的完全二叉树,则最后一个结点是第
个结点的子树。
2)筛选从第
个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
-
-
时间复杂度:Ο**(nlogn)
-
/** * 堆排序算法 */ void HeapSort(int H[],int length) { //初始堆 BuildingHeap(H, length); //从最后一个元素开始对序列进行调整 for (int i = length - 1; i > 0; --i) { //交换堆顶元素H[0]和堆中最后一个元素 int temp = H[i]; H[i] = H[0]; H[0] = temp; //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整 HeapAdjust(H,0,i); } } /** * 初始堆进行调整 * 将H[0..length-1]建成堆 * 调整完之后第一个元素是序列的最小的元素 */ void BuildingHeap(int H[], int length) { //最后一个有孩子的节点的位置 i= (length -1) / 2 for (int i = (length -1) / 2 ; i >= 0; --i) HeapAdjust(H,i,length); } /** *@Param heap是待调整的数组 *@Param s是待调整数组元素的位置 *@Param length是待调整数组的长度 **/ void HeapAdjust(int H[],int s, int length) { int tmp = H[s]; int child = 2*s+1; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置) while (child < length) { if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点) ++child ; } if(H[s]<H[child]) { // 如果较大的子结点大于父结点 H[s] = H[child]; // 那么把较大的子结点往上移动,替换它的父结点 s = child; // 重新设置s ,即待调整的下一个结点的位置 child = 2*s+1; } else { // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出 break; } H[s] = tmp; // 当前待调整的结点放到比其大的孩子结点位置上 } print(H,length); }
4.交换排序
4.1.冒泡排序
-
基本思想:
- 首先从数组的第一个元素开始到数组最后一个元素为止,对数组中相邻的两个元素进行比较,如果位于数组左端的元素大于数组右端的元素,则交换这两个元素在数组中的位置,此时数组最右端的元素即为该数组中所有元素的最大值。接着对该数组剩下的n-1个元素进行冒泡排序,直到整个数组有序排列。
-
时间复杂度:O(n^2)
-
void BubbleSort(int arr[], int length) { for (int i = 0; i < length; i++) { for (int j = 0; j < length - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp; temp = arr[j + 1]; arr[j + 1] = arr[j]; arr[j] = temp; } } }
-
冒泡排序改进:
-
1.加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。
-
//设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。 //由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。 public static void newBubbleSort_01(int arr[],int length){ int i=length-1; while(i>0){ int pos=0; //设置标志变量初始记载零号元素 for(int j=0;j<i;j++){ if(arr[j]>arr[j+1]){ int tmp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=tmp; pos=j; //发生交换时,记录位置 } } i=pos; //为下一趟循环设置上限 } }
-
2.传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半
-
public static void newBubbleSort(int[] arr,int length){ int low=0; int high=length-1; //设置变量最小值 while(low<high){ for(int i=0;i<high;i++){ //正向冒泡找到最大值 if(arr[i]>arr[i+1]){ int tmp=arr[i]; arr[i]=arr[i+1]; arr[i+1]=tmp; } } high--; //修改上限值,前移一位 for(int i=high;i>low;i--){ //反向冒泡找到最小值 if(arr[i]<arr[i-1]){ int tmp=arr[i]; arr[i]=arr[i-1]; arr[i-1]=tmp; } } low++; //修改下限值,后移一位 } }
-
4.2.快速排序
-
基本思想:
- 基于分治的递归算法
1、先从数列中取出一个数作为基准数
2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
3、再对左右区间重复第二步,直到各区间只有一个数
-
快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的,但初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序
-
当待排序的关键字是随机分布时,快速排序的平均时间最短;
-
时间复杂度:O(NlogN)
- 最坏情况下为O(N^2)
-
一次排序过程
- 排序全过程
-
private static void quickSort(int[] arr,int l,int r) { if (l<r) { int length = arr.length; int i=l,j=r; int x=arr[i]; //选择第一个数为基准数 while(i<j){ while(i<j&&arr[j]>x){ //从右至左寻找小于x的数 j--; } if(i<j) arr[i++]=arr[j]; while(i<j&&arr[i]<x){ //从左至右寻找大于x的数 i++; } if(i<j) arr[j--]=arr[i]; } arr[i]=x; quickSort(arr,l,i-1); //递归调用调整左右两个队列 quickSort(arr,i+1,r); } }
5.归并排序
-
基本思想:
- 归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,直至最终划分的子序列大小为1,然后再把有序子序列逐步合并为整体有序序列。
- 归并排序是一个典型的基于分治的递归算法,有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
-
时间复杂度:O(NlogN)
- 由归并排序的递归公式:T(N) = 2T(N/2) + O(N) 可知时间复杂度为O(NlogN)
-
空间复杂度:O(N)
- 归并排序中,用到了一个临时数组,故空间复杂度为O(N)
-
稳定性:归并排序是稳定的排序算法,
temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
这行代码可以保证当左右两部分的值相等的时候,先复制左边的值,这样可以保证值相等的时候两个元素的相对位置不变。 -
相关:
- 归并排序中的比较次数是所有排序中最少的。原因是,它一开始是不断地划分,比较只发生在合并各个有序的子数组时
- 因此,JAVA的泛型排序类库中实现的就是归并排序。因为:对于JAVA而言,比较两个对象的操作代价是很大的(根据Comparable接口的compareTo方法进行比较),而移动两个对象,其实质移动的是引用,代价比较小。(排序本质上是两种操作:比较操作和移动操作)j
- java.util.Arrays.sort(T[] arr)使用的是归并排序
- java.util.Arrays.sort(int[] arr) 使用的是快速排序
-
private static void mergeSort(int[] arr) { sort(arr,0,arr.length-1); //对原始数组进行切分 } private static void sort(int[] arr, int l, int r) { if(l==r) //左边际等于右边际时表示不能再切分,此时数组元素数量为一 return; int mid=(l+r)/2; //取中值 sort(arr,l,mid); //切分左边数组 sort(arr,mid+1,r); //切分右边数组 merge(arr,l,mid,r); //将两边已排序数组合成一个合并数组 } private static void merge(int[] arr, int l, int mid, int r) { int[] temp=new int[r-l+1]; //创立一个辅助数组暂存排完序的合并数组 int ele1=l; //左边数组的开头元素 int ele2=mid+1; //右边数组的开头元素 int i=0; while(ele1<=mid&&ele2<=r){ temp[i++]=arr[ele1]<=arr[ele2]?arr[ele1++]:arr[ele2++]; //将两边数组里的元素逐一比较后放入辅助数组 } while(ele1<=mid){ //两边数组里其余未放入辅助数组的数接到末尾 temp[i++]=arr[ele1++]; } while(ele2<=r){ temp[i++]=arr[ele2++]; } for (int j = 0; j < temp.length; j++) { arr[l+j]=temp[j]; //将辅助数组的数全部复制到原数组 } }
6.基数排序
-
基本思想:
- 基数排序又称为“桶子法”,从低位开始(个位)将待排序的数按照这一位的值放到相应的编号为0~9的桶中。等到低位排完得到一个子序列,再将这个序列按照次低位(十位)的大小进入相应的桶中,一直排到最高位为止,数组排序完成。
-
分类:
-
LSD——从低位向高位排(从个位开始排)
MSD——从高位向低位排
-
-
时间复杂度:O(d(r+n)),最差时O(d(r+n))
- r代表关键字基数,d代表长度,n代表关键字的个数
-
排序过程:
-
//LSD //arr是要排序的数组,max是数组中最大的数有几位 public static void radixSort(int[] arr,int max) { //count数组用来计数 int[] count = new int[10]; //bucket用来当桶(在下面你就理解了什么是桶了),放数据,取数据 int[] bucket = new int[arr.length]; //k表示第几位,1代表个位,2代表十位,3代表百位 for(int k=1;k<=max;k++) { //把count置空,防止上次循环的数据影响 for(int i=0;i<10;i++) { count[i] = 0; } //分别统计第k位是0,1,2,3,4,5,6,7,8,9的数量 //以下便称为桶 //即此循环用来统计每个桶中的数据的数量 for(int i=0;i<arr.length;i++) { count[getFigure(arr[i],k)]++; } //利用count[i]来确定放置数据的位置 for(int i=1;i<10;i++) { count[i] = count[i] + count[i-1]; } //执行完此循环之后的count[i]就是第i个桶右边界的位置 //利用循环把数据装入各个桶中,注意是从后往前装 //这里是重点,一定要仔细理解 for(int i=arr.length-1;i>=0;i--) { int j = getFigure(arr[i],k); bucket[count[j]-1] = arr[i]; count[j]--; } //将桶中的数据取出来,赋值给arr for(int i=0;i<arr.length;i++) { arr[i] = bucket[i]; } printAll(arr,k); } } //此函数返回整型数i的第k位是什么 public static int getFigure(int i,int k) { int[] a = {1,10,100}; return (i/a[k-1])%10; }
-
参考自