排序算法
性质记忆
1、关于稳定性
不稳定: 快选堆希(快速排序、选择排序、堆排序、希尔排序)
稳 定: 插冒归计基(简单插入排序、冒泡排序、归并排序、计数排序、基数排序)
2、关于时间复杂度
平方阶 (O(n2)) 排序
各类简单排序:直接插入、直接选择和冒泡排序。
线性对数阶 (O(nlog2n)) 排序
快速排序、堆排序和归并排序;
O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数
希尔排序
线性阶 (O(n)) 排序
基数排序,此外还有桶、箱排序。
3、关于移动次数和关键字顺序无关的排序
堆排序、归并排序、选择排序、基数排序
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
冒泡排序
每次遍历,都将最大的元素移到最后。
相邻两个元素之间的比较。
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
public void bulbSort(int[] arr) {
// 当前个与后一个相比,所以外循环只需n-1次
for (int i = 0; i < arr.length-1; i++) {
// 内循环比较大小,因为当第i次循环完后,最后的i+1个已排完序,下一次可以不用参与
// 如:3 1 4 2
// 第i=0次循环完(4-1-0=3次):1 3 2 4,最后一个排完序,下一次可以不用参与
// 第i=1次循环完(4-1-1=2次):1 2 3 4,最后两个排完序,下一次可以不用参与
for (int j = 0; j < arr.length - i - 1; j++) {
// 相邻元素两两对比,如果第一个比第二个大,就交换他们两个
if (arr[j]>arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
选择排序
在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
public void selectionSort(int[] arr) {
// 当前个与后一个相比,所以外循环只需n-1次
for (int i = 0; i < arr.length - 1; i++) {
// 每次循环,i前面的都是已经排完序了的
// 初始将当前i位置的数认为是最小的
int minIndex = i;
// 从i后面的所有数中,寻找值最小的数
for (int j = i+1; j < arr.length; j++) {
// 判断是否比当前已知的最小的数还要小
if (arr[j]<arr[minIndex]) {
// 将最小数的索引保存
minIndex = j;
}
}
// 如果两者不相等,说明存在比它更小的数,需要交换
if (i!=minIndex){
// 将i位置的数与最小值的数交换
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
插入排序
依次将元素插入对应位置,不符合的元素后移。
在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
当前元素与它前面所有元素的对比。
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
public void insertSort(int[] arr) {
// 第i=0个元素默认已经是有序,因此从i=1开始
// 每个元素都要参与比较,所以arr[n]也要取到
for (int i = 1; i < arr.length; i++) {
// 先记录下待插入的元素的值
int key = arr[i];
int j;
// 在已排序中,从后往前扫描
// 依次对比大小,大于它的往后移动,直至找到正确位置后停止
// j=i-1:前i-1个元素已经是排完序了的
for (j = i-1; j>=0 && arr[j]>key; j--) {
arr[j+1] = arr[j];
}
// 将值插入,注意上面的for循环出来前会执行一次j--,所以这里要j+1
arr[j+1] = key;
}
}
快速排序
使用分治法来把一个串(list)分为两个子串(sub-lists)。
左右指针相向移动,先从右指针开始;小的放左边,大的放右边。
挖坑法
- 先从数列中取出一个数作为基准数。i =L; j = R; 将基准数挖出形成第一个坑a[i]。
- j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
- i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
- 再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
public void quickSort(int[] arr, int left, int right) {
// 当left=right时,说明左右指针指向了同一个元素
// 即当前分组只有一个元素,递归结束
if (left>=right) {
return;
}else {
int l = left;
int r = right;
// 将左边第一个元素作为基准值
// 记录基准值,此时arr[l]这个位置可以放其他值了
int pivot = arr[l];
// 对于当前递归
// 循环比较,直至左右指针相遇,即l=r
while (l<r) {
// 先将右指针向左移动,直到遇到一个比基准值小的元素
while (l<r && arr[r]>pivot) {
r --;
}
// 如果指向了同一个元素,就不需要交换
// 否则,就需要交换
if (l<r) {
// 将右指针指向的元素值赋给左指针指向的元素值
// 此时arr[r]又可以存其他新的内容了
arr[l] = arr[r];
// 左指针已经填值,因此需要l++
l ++;
}
// 再将左指针向右移动,直到遇到一个比基准值大的元素
while (l<r && arr[l]<pivot) {
l ++;
}
// 同上,下同
if (l<r) {
arr[r] = arr[l];
r --;
}
}
// 此时l=r,说明左右指向了同一个位置
// 而这个位置应该放的是基准值,且一趟排序完成
arr[l] = pivot;
// 对基准值左边部分再进行递归排序
quickSort(arr, left, l-1);
// 对基准值右边部分再进行递归排序
// 由于此时l=r,所以r+1=l+1没影响
quickSort(arr, r+1, right);
}
}
归并排序
先拆分,再合并排序的算法
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
/**
* 合并排序部分
* @param arr 原始数组
* @param tempArr 临时数组
* @param left 当前分组的左边位置序号
* @param right 当前分组的右位置边序号
*/
public void merge(int[] arr, int[] tempArr, int left, int right) {
// 大于1个元素,还可继续划分
if (left < right) {
// 中间位置
int mid = (left+right)/2;
// 左半部分递归划分
merge(arr, tempArr, left, mid);
// 右半部分递归划分
merge(arr, tempArr, mid+1, right);
// 到这里,说明已经划分完,开始向上合并
// 指向左边组序列的第一个
int l = left;
// 指向右边组序列的第一个
int r = mid+1;
// 临时数组的下标,这里的临时数组是存排序后的元素,排完后再复制到原数组中
int tempIndex = left;
// 循环比较,直至左边组或右边组没有剩余元素了才停止
while (l<=mid && r<=right) {
// 可以想象最简单的情况,倒数第二层,有两个元素,需要对这两个元素排序
// 如果左边组的一个元素 < 右边组的一个元素,就将小的那个先放到临时数组中。放完后,下标+1指向下一个元素
if(arr[l]<arr[r]) {
tempArr[tempIndex++] = arr[l++];
}else {
tempArr[tempIndex++] = arr[r++];
}
}
// 可能左边组还有剩余元素,将剩余元素添加到临时数组最后
while (l<=mid) {
tempArr[tempIndex++] = arr[l++];
}
// 可能右边组还有剩余元素,将剩余元素添加到临时数组最后。这里与上面左边剩余的情况不会同时成立
while (r<=right) {
tempArr[tempIndex++] = arr[r++];
}
// 最后将排完顺序的临时数组中的元素复制到原数组中
while (left<=right) {
arr[left] = tempArr[left];
left++;
}
}
// 否则,说明已经划分到最小单元,即一个元素
else {
return;
}
}
public void mergeSort(int[] arr) {
// 开辟一个临时数组
int[] tempArr = new int[arr.length];
// 开始归并排序
merge(arr, tempArr, 0, arr.length-1);
}
public static void main(String[] args) {
int[] arr = new int[]{5,9,1,4,54,6,77,54,3,9,5,66,2,6,34};
new Main().mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
不管元素在什么情况下都要做这些步骤,所以花销的时间是不变的,所以该算法的最优时间复杂度和最差时间复杂度及平均时间复杂度都是一样的为:O( nlogn )
归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn
;所以空间复杂度为: O(n)
。
归并排序算法中,归并最后到底都是相邻元素之间的比较交换,并不会发生相同元素的相对位置发生变化,故是稳定性算法。
希尔排序
按组(增量)进行插入排序的算法。
通过对每组使用直接插入排序,使整个数组基本有序.数据有序程度越高,最后使用的插入排序效率越高。
当gap=4时,意味着将数列分为4个组: {80,20},{30,10},{60,50},{40,70}。 对应数列: {80,30,60,40,20,10,50,70}
对这4个组分别进行排序,排序结果: {20,80},{10,30},{50,60},{40,70}。 对应数列: {20,10,50,40,80,30,60,70}
当gap=2时,意味着将数列分为2个组:{20,50,80,60}, {10,40,30,70}。 对应数列: {20,10,50,40,80,30,60,70}
注意:{20,50,80,60}实际上有两个有序的数列{20,80}和{50,60}组成。
{10,40,30,70}实际上有两个有序的数列{10,30}和{40,70}组成。
对这2个组分别进行排序,排序结果:{20,50,60,80}, {10,30,40,70}。 对应数列: {20,10,50,30,60,40,80,70}
当gap=1时,意味着将数列分为1个组:{20,10,50,30,60,40,80,70}
注意:{20,10,50,30,60,40,80,70}实际上有两个有序的数列{20,50,60,80}和{10,30,40,70}组成。
对这1个组分别进行排序,排序结果:{10,20,30,40,50,60,70,80}
public void shellSort(int[] arr) {
// 每次增量除2
for (int inc = arr.length/2; inc > 0; inc/=2) {
// 每一趟使用插入排序,从第1组的第2个元素开始,到数组的最后一个元素
// 这里每趟都是对下一组进行排序,即各组交替执行,而不是一个组排序完后再换下一个组
for (int i = inc; i < arr.length; i++) {
// 先拿到当前位置的值key
int key = arr[i];
int j;
// 对当前组的当前位置及前面元素进行插入排序
// j>=inc:保证j-inc非负
// key<arr[j-inc]:将目标值与前一个元素的值比较
// j-=inc:指向当前组的前一个元素
for (j = i; j>=inc&&key<arr[j-inc]; j-=inc) {
// 将值后移
arr[j] = arr[j-inc];
}
// 将key插入,注意上面的for循环出来前,会执行一次j-=inc
arr[j] = key;
// 或者
/*
for (j = i-inc; j>=0 && arr[j]>key; j-=inc) {
arr[j+inc] = arr[j];
}
arr[j+inc] = key;
*/
}
}
堆排序
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。
- 不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
/**
* 堆排序
* @param arr 待排序数组
* @param n 总节点数
* @param i 当前节点的下标
*/
void heapify(int[] arr, int n, int i) {
// 记录最大值的节点的下标
int largest = i;
// 第i个节点的左子节点的下标:i*2+1
int lson = i*2+1;
// 第i个节点的右子节点的下标:i*2+2
int rson = i*2+2;
// 先比较根节点与左子节点的大小
// 如果左子节点的值更大,就记录左子节点的下标
if (lson<n && arr[lson]>arr[largest]) {
largest = lson;
}
// 再比较根节点(或左子节点,如果上面的if成立的话)与右子节点的大小
// 如果右子节点的值更大,就记录右子节点的下标
if (rson<n && arr[rson]>arr[largest]) {
largest = rson;
}
// 若不相等,则说明largest有变动过,即左或右子节点比根节点大了
if (largest != i) {
// 交换两者的值,把更大的值放到根节点上
int temp = arr[largest];
arr[largest] = arr[i];
arr[i] = temp;
// 递归执行堆维护,因为可能本次调整完后,影响了后面的堆性质,如:
// 3 4
// / \ / \
// 4 1 => 3 1
// / \ / \
// 5 2 5 2
// 3跟4换完后,3跟5也需要换
heapify(arr, n, largest);
}
}
/**
* 大顶堆,堆排序
* @param arr 待排序的数组
*/
public void heapSort(int[] arr) {
int n = arr.length;
// 1.建堆
// 第i个节点的根节点的下标:(i-1)/2
// 最后一个节点的根节点的下标:((n-1)-1)/2 = n/2-1
// 从最后一个根节点到0号根节点依次循环
for (int i = n/2-1; i >= 0; i--) {
// 从下往上维护堆,递归次数少,效率高
heapify(arr, n, i);
}
// 2.排序
// 将最大放到最后
// 由于大顶堆,所以最大值总是在0号节点上
// 从最后一个节点到0号节点依次循环
for (int i = n-1; i > 0; i--) {
// 交换两者的值,把最大值放到后面的节点上
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 交换完成后,对剩余的节点继续执行堆维护
// i: 由于每次都把最大值往后面放,所以这个i可以限制heapify不会影响到后面已排完序的节点
// 0: 从上往下维护堆,之前已经建完堆,这次可能只需要微调
heapify(arr, i, 0);
}