排序算法
排序算法
算法的稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
见另一篇md:
<C:\Users\gaoya\Documents\TyporaDoc\稳定排序与不稳定排序.md>
1.冒泡排序
重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。
1. 算法步骤
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2. 动图演示
public class BubbleSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
for (int i = 1; i < arr.length; i++) {
// 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
boolean flag = true;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = false;
}
}
//如果没有进行操作,说明后面数据已经有序
if (flag) {
break;
}
}
return arr;
}
}
2.选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
1.算法步骤
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
2. 动图演示
public class SelectionSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// 记录目前能找到的最小值元素的下标
min = j;
}
}
// 将找到的最小值和i位置所在的值进行交换
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
}
3.插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
(在排序的过程中,把数组的每一个元素按照大小关系,插入到前面有序区的对应位置。 )
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
1. 算法步骤
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
2. 动图演示
a.直接插入排序
InsertSort(int[] a){
int tmp;
for (int i = 1; i < a.length; i++){
tmp=a[i];
//从右向左比较
for(int j=i; j>0 && a[j-1]>tmp; j--){
a[j] = a[j-1]; //当前值比上一个值小,则令上一个值等于当前值
}
a[j]=tmp;
}
}
时间复杂度:两个嵌套循环,为 O(N2) 。若最好情况下,数组已经递增有序,则时间复杂度为O(N)
空间复杂度:由于插入排序是在原地进行排序,并没有引入额外的数据结构,所以是 O(1)
b.算法优化 ---> 二分插入排序
时间复杂度:折半插入排序减少了比较元素的次数,约为 O(NlogN),比较的次数取决于表的元素个数 n。因此,折半插入排序的时间复杂度仍然为 O(n²),但它的效果还是比直接插入排序要好。
空间复杂度:排序只需要一个位置来暂存元素,因此空间复杂度为 O(1)。
优点 : 稳定,相对于直接插入排序元素减少了比较次数
缺点 : 相对于直接插入排序元素的移动次数不变;
4.希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
package 插入排序;
public class 希尔排序 {
public static void main(String[] args) {
int[] arr = {1,3,4,7,2,0,9,5,8,6};
shellSort(arr);
//打印数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] +" ");
}
}
public static void shellSort(int[] arr) {
int gap = 1;
//Knuth序列设置gap
while(gap < arr.length/3)
gap = gap*3 + 1;
int j;
while(gap>0) {
//插入排序的方法
for(int i = gap; i< arr.length; i++) {
int tmp = arr[i];
for(j = i; j >= gap && arr[j-gap] > tmp; j-=gap) {
arr[j] = arr[j-gap];
}
arr[j] = tmp;
}
//gap减小
gap = (int) Math.floor(gap/3);
}
}
}
时间复杂度:最坏的情况,为 O(N2) 。 一般认为是O(N3/2)
hibbard增量序列 最坏情况是O(N3/2)
Sedgewick 增量序列最坏情况是O(N4/3)
空间复杂度:由于插入排序是在原地进行排序,并没有引入额外的数据结构,所以是 O(1)
5.归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间O(N) 。
2. 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
3. 动图演示
package 交换排序;
public class 归并排序 {
public static void mergeSort(int[] arr) {
if(arr == null || arr.length == 1)
return ;
//递归过程
sortProcess(arr, 0, arr.length-1);
}
public static void sortProcess(int[] arr, int L,int R) {
if(L == R) {
return ;
}
int mid = L + (R-L)/2; //防止溢出; 移位 R-L >> 1
//左边部分排序
sortProcess(arr, L, mid);
//右边部分排序
sortProcess(arr, mid+1, R);
//对已经排好序的左右合并,只是迭代,不是递归
merge(arr,L,mid,R);
}
//将两个排序好的数组合并放到一个新的数组中
public static void merge(int[] arr, int L, int mid, int R) {
int[] help = new int[R - L + 1];
int i= 0;
int p1 = L;
int p2 = mid + 1;
//两个指针都没越界的情况,比较插入
while (p1 <= mid && p2 <= R) {
//三目运算符
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
//有一个指针越界的情况 ,全部剩下的插入
while(p1 <= mid) {
help[i++] = arr[p1++];
}
while(p2 <= R) {
help[i++] = arr[p2++];
}
for(i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
public static void main(String[] args) {
int[] arr = {5,1,6,7,2,0};
mergeSort(arr);
//System.out.println(max);
print(arr);
}
public static void print(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
public void swap(int []arr,int i,int j) {
int tmp;
tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
分析:
时间复杂度:T(N) = a T(n/b) + O(Nd) , (公式待了解,左神视频中有讲,这里忘了,之后再看吧)O(N Log N)*,
空间复杂度:新建了辅助数组,额外空间复杂度O(N)
6.堆排序
二叉树: 1.满二叉树 2.**完全二叉树 **
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
分析:
堆排序的平均时间复杂度为 Ο(nlogn)。
时间复杂度:建立大根堆 花费 O(N),再执行互换操作,每次互换 花费 O(Log N), 所有 总时间是 O(N Log N),
空间复杂度:额外空间复杂度O(1)
-
1,堆结构的 heapInsert 与 heapify
-
2,堆结构的增大和减少
-
3,如果只是建立堆的过程,时间复杂度为O(N)
-
4,优先级队列结构,就是堆结构
算法步骤
- 把数据数组 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
package 选择排序;
public class 堆排序 {
public static void main(String[] args) {
int[] arr = {1,3,4,7,2,0,9,5,8,6};
heapSort(arr);
//打印数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] +" ");
}
}
private static void heapSort(int[] arr) {
if(arr.length < 2)
return;
for(int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
//将目前的最大值堆的顶点,交换到二叉堆的末尾
swap(arr, 0 , --size);
while(size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
//顶部数据交换后,变成末尾小的值,那么交换过来的值要向小找到合适的位置,下沉
private static void heapify(int[] arr, int index, int size) {
//左孩子
int left = index * 2 + 1;
//左孩子没越界的情况
while(left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left+1:left;
largest = arr[largest] > arr[index] ? largest : index;
//如果当前节点就是最大的,那就跳出,已经是这个方向上合适的位置了,不用交换
if(largest == index) {
break;
}
//交换子节点中大的到当前节点处
swap(arr, largest, index);
//index 下移到下面的左节点处
index = largest;
//向下继续判断下一行
left = left * 2 + 1;
}
}
//这里生成的是 max Heap :每一棵树的最大值都是它的父节点
private static void heapInsert(int[] arr, int index) {
//与父节点作比较
while(arr[index] > arr[(index-1) / 2]) {
swap(arr,index,(index-1)/2);
//index 指向 下一个父节点
index = (index-1)/2;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
7.快速排序
经典快排的问题在于,总是拿最后一个数作为 枢纽元 ,那就会受到数据初始情况的影响,如果数据一开始是顺序的话,那就是最坏的情况 O(N2) 。我觉得快排其实就是 分块递归排序
所以一般采用三数中值分割法 或者 随机获得一个值 来获得 枢纽元,作为每次分割的 标准 。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行 ,以此达到整个数据变成有序序列
算法步骤
1、先从数列中取出一个数作为基准数
2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
3、再对左右区间重复第二步,直到各区间只有一个数
可以用荷兰国旗问题来改进快速排序
(荷兰国旗问题)
给定一个数组arr,和一个数num,
请把小于num的数放在数组的 左边,
等于num的数放在数组的中间,
大于num的数放在数组的 右边。
分析:
时间复杂度:O(N log N)
额外空间复杂度:O(log N)
package 交换排序;
public class 随机快速排序 {
/**
*
* 时间复杂度O(N*logN),随机快排的额外空间复杂度O(logN)
*
* @param arr
*/
private static void quickSort(int[] arr) {
if(arr.length < 2)
return ;
quickSort(arr, 0, arr.length-1); //
}
//重载, 快排主方法
//随机快排
private static void quickSort(int[] arr, int L, int R) {
if(L < R) {
//随机选取一个数与最右边的数交换,作为枢纽元
swap(arr, R, L + (int)(Math.random() * (R - L + 1)));
//分
int[] p = partition(arr, L, R);
//递归
quickSort(arr, L, p[0] - 1);
quickSort(arr, p[1] + 1, R);
}
}
private static int[] partition(int[] arr, int L, int R) {
int less = L-1;
int more = R;
while(L < more) {
if(arr[L] < arr[R])
swap(arr, L++, ++less);
else if(arr[L] > arr[R]) {
swap(arr, L, --more);
}else {
L++;
}
}
//将最右边的base标志元素与最左边的more值交换
swap(arr, R, more);
//返回等于base值的左右边界
return new int[] {less+1, more};
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] arr = {2,3,1,4,9,4,7,6,1,4,5,4,9};
quickSort(arr);
for (int i : arr) {
System.out.print(i + " ");
}
}
}
桶排序
非基于比较的排序,与被排序的样本的实际数据状况有关系,实际中并不经常使用。
时间复杂度O(N), 额外空间复杂度O(N)
稳定的排序(可以做到稳定,用栈来实现“桶”)
其实现包括计数排序和基数排序两种 ,所设置的桶的数量不一样,
计数排序设置的桶的数量与数据的数量一样;
基数排序大数据时设置的桶分段
计数排序:
基数排序:
计数排序的思想
(1) 得到无序数组的取值范围
(2) 根据取值范围"创建"对应数量的"桶"
(3) 遍历数组,把每个元素放到对应的"桶"中 , 桶 中的数值就是每个元素出现的次数
(4) 按照顺序遍历桶中的每个元素,依次放到数组中,即可完成数组的排序。
"桶"是一种容器,这个容器可以用多种数据结构实现,包括数组、队列或者栈。
public class 桶排序 {
/*
*
*/
public static void main(String[] args) {
int arr[] = {1,3,4,3,4,0,9,5,8,6};
bucketSort(arr);
for (int i : arr) {
System.out.println(i);
}
}
public static void bucketSort(int[] arr) {
if(arr.length < 2)
return;
int max = Integer.MIN_VALUE;
int i = 0;
for(; i < arr.length;i++) {
max = Math.max(max, arr[i]);
}
//桶的长度为数据最大值+1
int[] bucket = new int[max + 1];
for(i = 0; i < arr.length; i++) {
bucket[arr[i]] ++;
}
int j = 0;
for(i = 0; i < bucket.length; i ++) {
//bucket[i]中有大于0 的数值,说明 arr中有 i 这个值的存在,
// bucket[i] 就是 i 的个数
while(bucket[i]-- > 0) {
arr[j++] = i;
}
}
}
}
1.计数排序
2.基数排序
例子:
给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且要求不能用非基于比较的排序。
分析:用桶排序,一定有一个空桶,同一个桶中的数值差一定不是最大值,肯定是相邻桶的数值差为最大差值
相邻桶的 前一桶的最大值 和后一桶的最小值一定是相邻的两个数
只需要计算这两个数的差值,就是一定是相邻两数的最大差值
public static int maxGap(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
int len = nums.length;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
//得到数据的左右边界值
for (int i = 0; i < len; i++) {
min = Math.min(min, nums[i]);
max = Math.max(max, nums[i]);
}
if (min == max) {
return 0;
}
//每个桶中只保存这个桶的最大及最小值
//长度为 数组的长度+1
boolean[] hasNum = new boolean[len + 1];
int[] maxs = new int[len + 1];
int[] mins = new int[len + 1];
int bid = 0;
for (int i = 0; i < len; i++) {
bid = bucket(nums[i], len, min, max);
mins[bid] = hasNum[bid] ? Math.min(mins[bid], nums[i]) : nums[i];
maxs[bid] = hasNum[bid] ? Math.max(maxs[bid], nums[i]) : nums[i];
//标志,说明修改过,非空桶
hasNum[bid] = true;
}
int res = 0;
int lastMax = maxs[0];
int i = 1;
for (; i <= len; i++) {
if (hasNum[i]) {
//计算当前桶的最小值与上一个桶的最大值之间的差值
res = Math.max(res, mins[i] - lastMax);
lastMax = maxs[i];
}
}
return res;
}
//求数组中每个数值应该存在哪个桶
public static int bucket(long num, long len, long min, long max) {
return (int) ((num - min) * len / (max - min));
}