一、常见算法比较
图片名词解释:
- n: 数据规模
- k: “桶”的个数
- In-place: 占用常数内存,不占用额外内存
- Out-place: 占用额外内存
二、概括及其实现
(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。元素相等不交换,如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。 每趟排序必确定一个数值的最终位置。
代码实现:
package bubbleSort;
public class Bubblesort {
public static void main(String[] args) {
//冒泡排序算法
int[] numbers=new int[]{1,5,8,2,3,9,4};
int i,j;
for(i=0;i<numbers.length-1;i++)
{
for(j=0;j<numbers.length-1-i;j++)
{
if(numbers[j]>numbers[j+1])
{
int temp=numbers[j];
numbers[j]=numbers[j+1];
numbers[j+1]=temp;
}
}
}
System.out.println("从小到大排序后的结果是:");
for(i=0;i<numbers.length;i++)
System.out.print(numbers[i]+" ");
}
}
从小到大排序后的结果是:
1 2 3 4 5 8 9
(2)选择排序
选择排序的基本思想:
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
每一趟在n-i+1(i=1,2,3…,n-1)个记录中选取关键字最小的记录与第i个记录交换,并作为有序序列中的第i个记录。
例如:
待排序列: 43,65,4,23,6,98,2,65,7,79
第一趟: 2,65,4,23,6,98,43,65,7,79
第二趟: 2,4,65,23,6,98,43,65,7,79
第三趟: 2,4,6,23,65,98,43,65,7,79
第四趟: 2,4,6,7,43,65,98,65,23,79
第五趟: 2,4,6,7,23,65,98,65,43,79
第六趟: 2,4,6,7,23,43,98,65,65,79
第七趟: 2,4,6,7,23,43,65,98,65,79
第八趟: 2,4,6,7,23,43,65,65,98,79
第九趟: 2,4,6,7,23,43,65,65,79,98
选择排序的时间复杂度为:O(n^2),空间复杂度:O(1)
代码实现:
package sort;
public class SelectSort {
public static void main(String[] args) {
//模拟数据
int[] array = {52,63,14,59,68,35,8,67,45,99};
System.out.println("原数组:");
for (int i : array) {
System.out.print(i+" ");
}
System.out.println();
selectSort(array);
System.out.println("排序后:");
for (int i : array) {
System.out.print(i+" ");
}
}
public static void selectSort(int[] arr){
for(int i = 0; i < arr.length-1; i++){
int min = i;
for(int j = i+1; j <arr.length ;j++){
if(arr[j]<arr[min]){
min = j;
}
}
if(min!=i){
swap(arr, i, min);
}
}
}
//完成数组两元素间交换
public static void swap(int[] arr,int a,int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
原数组:
52 63 14 59 68 35 8 67 45 99
排序后:
8 14 35 45 52 59 63 67 68 99
(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
代码实现:
package bubbleSort;
public class InsertSort {
public static void main(String[] args){
int[] a = new int[]{43, 65, 4, 23, 6, 98, 2, 65, 7, 79};
System.out.println("插入排序:");
bInsertSort(a);
for(int i=0;i<10;i++){
System.out.print(a[i]+" ");
}
}
public static void bInsertSort(int a[]) //插入排序
{
int low,high,mid;
int temp;
for(int i=1;i<a.length;i++)
{
low=0;
//把a[i]元素插入到它的前面a[0--(n-1)]中
temp=a[i];
high=i-1;
while(low<=high) //该while是折半优化算法,缩小a[i]的范围
{
mid=(low+high)/2;
if(a[mid]>temp)
high=mid-1;
else
low=mid+1;
}
int j=i;
while((j>low)&&(a[j-1]>temp)) //让a与已经排好的数组的各元素比较,小的放前面
{
a[j]=a[j-1];
--j;
}
a[low]=temp;
}
}
}
插入排序:
2 4 6 7 23 43 65 65 79 98
Program ended with exit code: 0
(4)快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
代码实现:Java
public class QuickSort {
#arr 需要排序的数组
#low 开始时最左边的索引=0
#high 开始时最右边的索引=arr.length-1
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;#左边哨兵的索引
j=high;#右边哨兵的索引
//temp就是基准位
temp = arr[low];#以最左边为 基准位
while (i<j) {
//先看右边,依次往左递减
#先从右往左找一个小于 基准位的数
#当右边的哨兵位置所在的数>基准位的数 时
#继续从右往左找(同时 j 索引-1)
#找到后会跳出 while循环
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
#步骤和上面类似
while (temp>=arr[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
#z、y 都是临时参数,用于存放 左右哨兵 所在位置的数据
int z = arr[i];
int y = arr[j];
# 左右哨兵 交换数据(互相持有对方的数据)
arr[i] = y;
arr[j] = z;
}
}
#这时 跳出了 “while (i<j) {}” 循环
#说明 i=j 左右在同一位置
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];#或 arr[low] = arr[j];
arr[i] = temp;#或 arr[j] = temp;
#i=j
#这时 左半数组<(i或j所在索引的数)<右半数组
#也就是说(i或j所在索引的数)已经确定排序位置, 所以就不用再排序了,
# 只要用相同的方法 分别处理 左右数组就可以了
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}
public static void main(String[] args){
int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
quickSort(arr, 0, arr.length-1);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
输出为
1
2
2
3
4
4
7
7
8
9
10
19
62
(5)归并排序(分治法)
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
详细图解:https://www.cnblogs.com/chengxiao/p/6194356.html
package sortdemo;
import java.util.Arrays;
/**
* Created by chengxiao on 2016/12/8.
*/
public class MergeSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
}
(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
代码实现:
#include<iostream>
using namespace std;
//该函数的作用是找出num的pos位数的数字(比如:23的个位数数字是3)
int getNumPos(int num,int pos)
{
int i;
int temp=1;
for(i=0;i<pos-1;i++)
temp*=10;
return (num/temp)%10;
}
void radixSort(int a[],int n) //基数排序
{
int i,j,k,pos,num,index;
//这几句话是创建一个从0-9(行)× (n+1)(列)的网格,第一列从上往下是0-9,第二列是该行包含的元素个数,默认为0个
int *radixArrays[10];
for(i=0;i<10;i++)
{
radixArrays[i]=(int *)malloc(sizeof(int)*(n+1));
radixArrays[i][0]=0;
}
//pos最大为31为数,计算机能承受的最大范围了
for(pos=1;pos<=31;pos++)
{
//该for循环是将数组的元素按照位数(pos)的值放进网格内
for(i=0;i<n;i++)
{
num=getNumPos(a[i], pos);
index=++radixArrays[num][0];
radixArrays[num][index]=a[i];
}
//该for循环是将上面的for循环已经按照某个位数(pos)排列好的元素存入数组
for(i=0,j=0;i<10;i++)
{
for(k=1;k<=radixArrays[i][0];k++)
a[j++]=radixArrays[i][k];
//清空网格,以便给下个位数排列
radixArrays[i][0]=0;
}
}
}
int main()
{
int a[10] = {43, 65, 4, 23, 6, 98, 2, 65, 7, 79};
cout<<"基数排序:"<<endl;
radixSort(a, 10);
for(int i=0;i<10;i++)
cout<<a[i]<<" ";
cout<<endl;
return 0;
}
执行结果:
基数排序:
2 4 6 7 23 43 65 65 79 98
Program ended with exit code: 0
(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
详细图解:https://www.cnblogs.com/chengxiao/p/6104371.html
package DataStr;
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = SortTestHelper.getRandomArray(15, 0, 10);
System.out.println("希尔排序前:"+Arrays.toString(arr));
shellSort(arr);
System.out.println("希尔排序后:"+Arrays.toString(arr));
}
/**
* 希尔排序
* @param arr 待排数组
*/
public static void shellSort(int[] arr) {
for(int gap=arr.length/2; gap>0; gap/=2) { /*步长逐渐减小*/
for(int i=gap; i<arr.length; i++) { /*在同一步长内*/
//同一步长内排序方式是插入排序
int temp = arr[i], j; //待排元素
//j-gap代表有序数组中最大数的下标,j-pag表示有序数组的前一个元素,减pag是减去偏移量就是步长
for(j=i; j>=gap && temp<arr[j-gap]; j-=gap)
arr[j] = arr[j-gap]; //原有序数组最大的后移一位
arr[j] = temp; //找到了合适的位置插入
}
}
}
}
(8)堆排序
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
代码实现:
package DataStr;
import java.util.Arrays;
/**
* 数组下标0的元素也参与排序
* @author stoneWang_L
*
*/
public class HeapSort3 {
public static void main(String[] args) {
int[] arr = {312,126,272,226,28,165,123,8,12};
// int[] arr = SortTestHelper.getRandomArray(20, 0, 2);
System.out.println("堆排序前:"+Arrays.toString(arr));
//堆排序
heapSort(arr, arr.length);
System.out.println("堆排序后:"+Arrays.toString(arr));
}
private static void heapSort(int[] arr, int count) {
//完成构建大顶堆
for(int i=(count-2)/2; i>=0; i--)
shiftDown(arr, count, i);
System.out.println("构建完成的大顶堆:"+Arrays.toString(arr));
//将大顶堆头结点(最大数)与最后一个元素交换位置,然后除去最后这个最大的元素,再shiftDown操作维护该(除去最后一个元素的数组)堆为大顶堆。循环直到升序排序完成
for(int j=count-1; j>0; j--) {
swap(arr, 0, j);
shiftDown(arr, j, 0);
}
}
/**
*
* @param arr 数组
* @param count 元素个数
* @param currentRoot 当前根节点的下标
*/
private static void shiftDown(int[] arr, int count ,int currentRoot) {
while(2*currentRoot+1 < count) {
int max = 2*currentRoot+1; //初始赋值left孩子
if(max+1 < count && arr[max+1] > arr[max])
max += 1;
if(arr[currentRoot] >= arr[max])
break;
swap(arr, currentRoot, max);
currentRoot = max;
}
}
//交换
public static void swap(int[] arr, int n, int m) {
int temp = arr[n];
arr[n] = arr[m];
arr[m] = temp;
}
}
执行结果: