八大排序
参考:https://jackcui.blog.csdn.net/article/details/78979946
参考:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
交换排序
冒泡排序
原理
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
Java代码:
public static void main(String[] args) {
int[] result = {2,4,1,3,6,5};
int temp;
System.out.println("----冒泡排序前顺序----");
for (int i : result) {
System.out.print(i);
}
for(int i=0;i<result.length-1;i++){
for(int j = 0;j<result.length-1-i;j++){
if(result[j+1]<result[j]){
//后一个比前一个小
temp = result[j];
result[j] = result[j+1];
result[j+1] = temp;
}
}
}
System.out.println();
System.out.println("----冒泡排序后结果----");
for (int i : result) {
System.out.print(i);
}
}
结果:
----冒泡排序前顺序----
241365
----冒泡排序后结果----
123456
复杂度
时间复杂度:
所以,冒泡排序最好的时间复杂度为
空间复杂度:
只有一个temp,所以空间复杂度为O(1)
算法改进
对冒泡排序常见的改进方法是加入标志性变量exchange,用于标志某一趟排序过程中是否有数据交换。如果进行某一趟排序时并没有进行数据交换,则说明所有数据已经有序,可立即结束排序,避免不必要的比较过程。
算法稳定性
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
快速排序
排序流程
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
(2)**将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。**此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
原理
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]的值交换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换;
5)重复第3、4步,直到i==j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key**,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。**
Java代码
public class Sort {
public static void main(String[] args) {
int[] result = {2,4,1,3,6,5};
int temp;
System.out.println("----快速排序前顺序----");
for (int i : result) {
System.out.print(i);
}
//快速排序
Quickleft_right(result,0,result.length-1);
System.out.println();
System.out.println("----快速排序后结果----");
for (int i : result) {
System.out.print(i);
}
}
public static void Quickleft_right(int[] result,int left,int right){
if(left<right){
//找到中间元素的位置
int base = Quick(result,left,right);
// 递归调用
Quickleft_right(result,0,base-1);
Quickleft_right(result,base+1,right);
}
}
public static int Quick(int []result, int left, int right){
int first = result[left];
while(left<right){
//从右到左,找到第一个小于first的元素
while(left<right && result[right]>=first)
right--;
// 找到了比base小的元素,将这个元素放到最左边的位置
result[left] = result[right];
//从左到右,找到第一个大于first的元素
while(left<right && result[left]<=first)
left++;
// 找到了比base大的元素,将这个元素放到最右边的位置
result[right] = result[left];
}
result[left] = first;
return left;
}
}
复杂度
时间复杂度:
快速排序的一次划分算法从两头交替搜索,直到low和hight重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)。
为改善最坏情况下的时间性能,可采用其他方法选取中间数。通常采用“三者值取中”方法,即比较H->r[low].key、H->r[high].key与H->r[(low+high)/2].key,取三者中关键字为中值的元素为中间数。
可以证明,快速排序的平均时间复杂度也是O(nlog2n)。因此,该排序方法被认为是目前最好的一种内部排序方法。
- 当数据有序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差。
- 而当数据随机分布时,以第一个关键字为基准分为两个子序列,两个子序列的元素个数接近相等,此时执行效率最好。
- 所以,数据越随机分布时,快速排序性能越好;数据越接近有序,快速排序性能越差。
空间复杂度:
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1);但最坏的情况下,栈的最大深度为n。这样,快速排序的空间复杂度为O(log2n)。
算法稳定性
在快速排序中,相等元素可能会因为分区而交换顺序,所以它是不稳定的算法。
插入排序
直接插入排序
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动 。
基本思想
插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。拿在左手上的牌总是排序好的,原来这些牌是桌子上牌堆中顶部的牌 。
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序 。
Java代码
public class Insertion
{
public static void sort(Comparable[] a)
{
//将a[]按升序排列
int N=a.length;
for (int i=1 ;i<N;i++)
{
//将a[i]插入到a[i-1],a[i-2],a[i-3]……之中
for(int j=i;j>0&&(a[j].compareTo(a[j-1])<0);j--)
{
Comparable temp=a[j];
a[j]=a[j-1];
a[j-1]=temp;
}
}
}
}
算法分析
时间复杂度
当数据正序时,执行效率最好,每次插入都不用移动前面的元素,时间复杂度为O(N)。
当数据反序时,执行效率最差,每次插入都要前面的元素后移,时间复杂度为O(N^2)。
所以,数据越接近正序,直接插入排序的算法性能越好。
空间复杂度
由直接插入排序算法可知,我们在排序过程中,需要一个临时变量存储要插入的值,所以空间复杂度为 1 。
算法稳定
直接插入排序的过程中,不需要改变相等数值元素的位置,所以它是稳定的算法。
优化
因为在一个有序序列中查找一个插入位置,以保证有序序列的序列不变,所以可以使用二分查找,减少元素比较次数提高效率。
二分查找是对于有序数组而言的,假设如果数组是升序排序的。那么,二分查找算法就是不断对数组进行对半分割,每次拿中间元素和目标数字进行比较,如果中间元素小于目标数字,则说明目标数字应该在右侧被分割的数组中,如果中间元素大于目标数字,则说明目标数字应该在左侧被分割的数组中。
希尔排序
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
基本思想
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2 ,…一直下去。该方法实质上是一种分组插入方法比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。
Java代码
public class Shell {
public static void main(String[] args) {
int[] num = {0,2,3,5,4,1,6,9,8,7};
shellSort(num);
for(int i: num){
System.out.print(i+" ");
}
}
public static void shellSort(int[] num) {
//控制列数 比如从5列到2列 到1列 就完事儿
for(int column = num.length/2; column > 0; column /= 2){
//从第一列 二的一个开始 一个一个走
for(int i = column; i < num.length; i++){
int tmp = num[i];
//定位最后的位置
int j;
//比较一列 然后如果当前需要比较的值小于前一个值 就将前一个值赋值到后一个 循环这一列 直到最开始
for(j = i; j >= column && num[j - column] > tmp; j -= column){
num[j] = num[j - column];
}
//最后修改的位置为j 所以将j位置赋值为tmp
num[j] = tmp;
}
}
}
}
优势
- 不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。
- 希尔排序的时间的时间复杂度为O(n3/2),希尔排序时间复杂度的下界是n*log2n。
- 希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O( )复杂度的算法快得多。
- 并且希尔排序非常容易实现,算法代码短而简单。
- 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。
选择排序
简单选择排序
原理
工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
Java代码
public class Sort {
public static void main(String[] args) {
int[] result = {2,4,1,3,6,5};
int temp;
System.out.println("----简答选择排序前顺序----");
for (int i : result) {
System.out.print(i);
}
//简单选择排序
int len = result.length;
for(int i=0; i<len; i++){
//记录当前位置
int position = i;
//找出最小的数,并用position指向最小数的位置
for(int j=i+1; j<len; j++){
if(result[j]<result[position]) {
position=j;
}//endif
}//endfor
//交换最小数data[position]和第i位数的位置
temp = result[position];
result[position] = result[i];
result[i] = temp;
}//endfor
System.out.println();
System.out.println("----简答选择排序后结果----");
for (int i : result) {
System.out.print(i);
}
}
}
复杂度
最好情况下,即待排序记录初始状态就已经是升序排列了,则不需要移动记录。
最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从大到小顺序排列,则需要移动记录的次数最多为3(n-1)。简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为O(n2),进行移动操作的时间复杂度为O(n2)。
算法稳定性
由于会改变相对位置,所以简单选择排序是不稳定排序。
堆排序
原理
堆是一棵顺序存储的完全二叉树。
- 其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。
- 其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大****根堆。
举例来说,对于n个元素的序列{R0, R1, … , Rn}当且仅当满足下列关系之一时,称之为堆:
- Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
- Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
其中i=1,2,…,n/2向下取整;
如上图所示,序列R{3, 8, 15, 31, 25}是一个典型的小根堆,元素3在数组中以R[0]表示,它的左孩子结点是R[1],右孩子结点是R[2]。
Java代码
import java.util.ArrayList;
public class tets {
public static void main(String[] args) {
int[] result = {2,4,1,3,6,5};
int temp;
System.out.println("----快速排序前顺序----");
for (int i : result) {
System.out.print(i);
}
heapSort(result);
System.out.println();
System.out.println("----快速排序后结果----");
for (int i : result) {
System.out.print(i);
}
}
public static int[] heapSort(int[] array) {
//这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1
for (int i = array.length / 2 - 1; i >= 0; i--) {
adjustHeap(array, i, array.length); //调整堆
}
// 上述逻辑,建堆结束
// 下面,开始排序逻辑
for (int j = array.length - 1; j > 0; j--) {
// 元素交换,作用是去掉大顶堆
// 把大顶堆的根元素,放到数组的最后;换句话说,就是每一次的堆调整之后,都会有一个元素到达自己的最终位置
swap(array, 0, j);
// 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。
// 接下来我们需要排序的,就是已经去掉了部分元素的堆了,这也是为什么此方法放在循环里的原因
// 而这里,实质上是自上而下,自左向右进行调整的
adjustHeap(array, 0, j);
}
return array;
}
/**
* 整个堆排序最关键的地方
* @param array 待组堆
* @param i 起始结点
* @param length 堆的长度
*/
//核心代码!!!
public static void adjustHeap(int[] array, int i, int length) {
// 先把当前元素取出来,因为当前元素可能要一直移动
int temp = array[i];
for (int k = 2 * i + 1; k < length; k = 2 * i + 1) { //2*i+1为左子树i的左子树(因为i是从0开始的),2*k+1为k的左子树
// 让k先指向子节点中最大的节点
if (k + 1 < length && array[k] < array[k + 1]) { //如果有右子树,并且右子树大于左子树
k++;
}
//如果发现结点(左右子结点)大于根结点,则进行值的交换
if (array[k] > temp) {
swap(array, i, k);
// 如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,循环对子节点所在的树继续进行判断
i = k;
} else { //不用交换,直接终止循环
break;
}
}
}
/**
* 交换元素
* @param arr
* @param a 元素的下标
* @param b 元素的下标
*/
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
使用场景:TopK问题
题目一:最小的K个数
题目描述
给定一个数组,找出其中最小的K个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。如果K>数组的长度,那么返回一个空的数组
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
//这里使用堆排序来解决
ArrayList<Integer> result = new ArrayList<Integer>();
if(input.length==0||k>input.length) return result;
heapSort(input,input.length-1,k,result);
return result;
}
public void heapSort(int[] input,int end,int k,ArrayList<Integer> result){
//建初始堆
for(int i = input.length/2-1;i>=0;i-- ){
adjustHeap(input,i,input.length-1);
}
//寻找最小的k个数
for(int i=input.length-1;i>=0;i--){
swap(input,0,i);//堆顶与最后一个交换
adjustHeap(input,0,i);
if(k>0){
result.add(input[i]);
k--;
}
}
}
public void adjustHeap(int []input,int i,int length){
int temp = input[i];
for(int j = i*2+1;j<length;j = i*2+1){
if(j+1<length&&input[j]>input[j+1]){
j++;
}
if(temp>input[j]){
swap(input,i,j);
i = j;
}else{
break;
}
}
}
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
原理
归并操作的工作原理如下:
- 第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针超出序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
Java代码
//递归方法
package MergeSort;
public class MergeSort {
public static int[] mergeSort(int[] nums, int l, int h) {
if (l == h) //左右相遇的时候
return new int[] { nums[l] };
int mid = l + (h - l) / 2;
int[] leftArr = mergeSort(nums, l, mid); //左有序数组
int[] rightArr = mergeSort(nums, mid + 1, h); //右有序数组
int[] newNum = new int[leftArr.length + rightArr.length]; //新有序数组
int m = 0, i = 0, j = 0;
//分别比较左数组与右数组的大学
while (i < leftArr.length && j < rightArr.length) {
newNum[m++] = leftArr[i] < rightArr[j] ? leftArr[i++] : rightArr[j++];
}
//如果左数组还有空余
while (i < leftArr.length)
newNum[m++] = leftArr[i++];
//如果右数组有空余
while (j < rightArr.length)
newNum[m++] = rightArr[j++];
return newNum;
}
public static void main(String[] args) {
int[] nums = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 10 };
int[] newNums = mergeSort(nums, 0, nums.length - 1);
for (int x : newNums) {
System.out.println(x);
}
}
}
时间复杂度
归并排序比较占用内存,但却是一种效率高且稳定的算法。
改进归并排序在归并时先判断前段序列的最大值与后段序列最小值的关系再确定是否进行复制比较。如果前段序列的最大值小于等于后段序列最小值,则说明序列可以直接形成一段有序序列不需要再归并,反之则需要。所以在序列本身有序的情况下时间复杂度可以降至O(n)
TimSort可以说是归并排序的终极优化版本,主要思想就是检测序列中的天然有序子段(若检测到严格降序子段则翻转序列为升序子段)。在最好情况下无论升序还是降序都可以使时间复杂度降至为O(n),具有很强的自适应性。
最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
传统归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | T(n) | 稳定 |
改进归并排序 [1] | O(n) | O(nlogn) | O(nlogn) | T(n) | 稳定 |
TimSort | O(n) | O(nlogn) | O(nlogn) | T(n) | 稳定 |
空间复杂度
由前面的算法说明可知,算法处理过程中,需要一个大小为n的临时存储空间用以保存合并序列。
算法稳定
在归并排序中,相等的元素的顺序不会改变,所以它是稳定的算法。
使用场景
题目描述:
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
如果两个区间为[4, 3] 和[1, 2]
那么逆序数为(4,1),(4,2),(3,1),(3,2),同样的如果区间变为有序,比如[3,4] 和 [1,2]的结果是一样的,也就是说区间有序和无序结果是一样的。但是如果区间有序会有什么好处吗?当然,如果区间有序,比如[3,4] 和 [1,2]
如果3 > 1, 显然3后面的所有数都是大于1, 这里为 4 > 1, 明白其中的奥秘了吧。所以我们可以在合并的时候利用这个规则。
代码
public class Solution {
int result = 0;
int[] temp;
public int InversePairs(int [] array) {
temp = new int[array.length];
Mergsort(array,0,array.length-1);
return result;
}
public void Mergsort(int []array,int first,int last){
if(first>=last) return;
int mid = first +((last-first)/2);
Mergsort(array,first,mid);
Mergsort(array,mid+1,last);
Mergsort_l(array,first,mid,last);
}
public void Mergsort_l(int []array,int first,int mid,int last){
int i =first,j = mid+1,k=0;
while(i<=mid&&j<=last){
if(array[i]>=array[j]){//这块与归并排序有些区别,归并排序时array[i]<array[j]
temp[k++] = array[j++];
result += (mid-i+1);//奥妙之处
result %= 1000000007;
}else{
temp[k++] = array[i++];
}
}
while(i<=mid){
temp[k++] = array[i++];
}
while(j<=last){
temp[k++] = array[j++];
}
for( k = 0,i = first;i<=last;++i,++k){
array[i] = temp[k];
}
}
}
基数排序
内功修炼不足,下次继续修炼!