常用排序算法

来源:互联网 发布:淘宝上卖美女图片 编辑:程序博客网 时间:2024/06/07 19:43

写在前面


内部排序算法及其改进算法很多种,但常用的无非以下 7 个。

基本排序算法

待排序数量不大(< 10K),平均时间复杂度 O(n^2)

  • 冒泡排序
  • 直接选择排序
  • 直接插入排序
  • 希尔排序(介于基本排序和高效排序之间)

高效排序算法

待排序数量较大,平均时间复杂度 O(nlogn)

  • 快速排序
  • 堆排序
  • 归并排序

冒泡排序


简介

冒泡排序是入门级排序算法,因其实现和理解较为简单而被广泛使用,平均时间复杂度为O(n^2),由于在排序过程中两两比较,如果相同不作互换,所以是一种稳定的排序算法。

思路

进行 n 次冒泡,n 为待排序元素个数。每次冒泡产生一个当前未排序元素中最大(小)的元素。

  1. 如第一次冒泡,从第一个元素开始,和第二个元素进行比较,如果大于第二个元素,则交换位置。 继续将第二个元素与第三个进行比较,如果大于第三个元素,则交换位置。类似的两两比较直到最后一个元素也被比较。由于每次都把相对大的元素向后推,最终最后一个元素就是最大的。
  2. 第 1 步操作完成一个元素的冒泡,而进行第二次冒泡时,只需进行n - 1次比较,因为最大的元素已经排好序了在最后一个。同理,进行第 i次冒泡时,只需进行n - i次比较。由此,进行n次冒泡,每次比较n-i次即可完成冒泡排序。

代码

public class BubbleSort {    public static void bubbleSort(int a[]){        int n = a.length;        for(int i =0 ; i< n-1; i++) {   //进行n次冒泡            for(int j = 0;j < n-i-1;j++) {  //进行n-i次比较                if(a[j] > a[j+1])                  {//交换位置                    int tmp = a[j] ;                     a[j] = a[j+1] ;                      a[j+1] = tmp;                  }              }          }      }     public static void main(String[] args) {        int[] arr = new int[]{2,7,3,0,5,4};        bubbleSort(arr);        for(int i = 0;i < arr.length;i++){            System.out.print(arr[i] + ",");        }    }}

直接选择排序


直接选择排序理解起来相对容易,平均时间复杂度为O(n^2)直接选择排序在排序过程涉及到两个元素位置的交换,所以相同数值的元素的排列顺序很可能在交互过程发生改变,所以是一种不稳定的排序算法。

思路

进行 n 次选择,n 为待排序元素个数,每次选择即排好一个元素。

示例待排序元素:【2,7,3,0】,下面高亮为已经排好序的。

  1. 以上面待排序元素为例,首先选择第一个元素2,再从2后面所有元素中找出最小的元素,可通过依次比较每次存下最小值位置,遍历到最后发现0元素为最小元素,将20互相交换位置。目前序列为:【0,7,3,2】。
  2. 接着选择元素7,找出7后面最小元素,发现是272交换位置。目前序列为:【02,3,7】。
  3. 同理,直到选择到最后一个元素,排序结束。

代码

public class StraightChooseSort {    public static void chooseSort(int a[]){        int n = a.length;        for(int i = 0;i < n;i++){            int k = i;//记录a[i]到a[n-1]中找最小的元素的位置            for(int j = i + 1;j < n;j++){                if(a[j] < a[k])                    k = j;            }            //交换位置            if(k != i){                int tmp = a[i] ;                 a[i] = a[k] ;                  a[k] = tmp;             }        }    }    public static void main(String[] args) {        int[] arr = new int[]{2,7,3,0,5,4};        chooseSort(arr);        for(int i = 0;i < arr.length;i++){            System.out.print(arr[i] + ",");        }    }}

直接插入排序


直接插入排序理解起来也相对容易,平均时间复杂度为O(n^2)直接插入排序在与前一个进行比较时,如果相等就排在其后面,所以是一种稳定的排序算法。

思路

进行 n-1 次插入,n 为待排序元素个数,每次插入即排好一个元素。

示例待排序元素:【2,7,3,0】,下面高亮为已经排好序的。

  1. 以上面待排序元素为例,首先从第二个元素7开始,先记下7这个值,然后与前一个元素2进行比较,发现2比自己小,不做移动。目前序列为:【27,3,0】。
  2. 接着从第三个元素3开始,先记下3这个值,与前一个元素7进行比较,发现73大,将7往后移动一个位置,即现在73的位置。接着再与前一个2比较,发现23小,不做移动,将3插入到2后面,即原先7的位置。目前序列为:【237,0】。
  3. 接着从第三个元素0开始,先记下0这个值,与前一个元素3进行比较,发现30大,将3往后移动一个位置;接着与前一个7进行比较,发现70大,将7往后移动一个位置;接着与前一个2进行比较,发现20大,将2往后移动一个位置。这时由于前面已经没有元素,所以把0 插入现在所在位置,即第一个。目前序列为:【0237】。

代码

public class StraightInsertionSort {    private static void InsertSort(int[] arr){        int temp;        for(int i = 1;i < arr.length;i++){            if(arr[i] < arr[i - 1]){    //从第二个开始,如果当前比前一个小                temp = arr[i];      //记录要插入的值                int j = i - 1;      //从前一个开始                do{                 //依次往后移动一个位置                    arr[j + 1] = arr[j];                    j--;                }while(j >= 0 && arr[j] > temp);//当且仅当遇到的数都比要插入的数大                arr[j + 1] = temp;//找到位置,插入            }        }    }    public static void main(String[] args) {        int[] arr = new int[]{2,7,3,0,5,4};        InsertSort(arr);        for(int i = 0;i < arr.length;i++){            System.out.print(arr[i] + ",");        }    }}

希尔排序


希尔排序又叫缩小增量排序,它是在直接插入排序的基础上,引入增量的做法,使得排序效率更高。时间复杂度约为n的1.2次幂,介于基本排序算法和高效排序算法间。由于希尔排序跳跃式的移动,使得值相同的元素可能被打乱顺序,因而它是不稳定排序。

思路

希尔排序提出缩小增量的概念在于解决直接插入排序存在的问题:没一个元素在插入之前,都需要使大量被比较的元素移动位置。

希尔排序通过不断划分子序列并在子序列里进行直接排序,这样一来每个子序列相当于跨间隔进行比较,举个例子:按间隔为10划分后,可能第1个元素和第10个元素在同一个子序列,原先直接插入排序可能需要多次移动才能将第1个元素和第10个元素进行排序,现在跨间隔只需要1次。而经过多次缩小间隔划分后,所有元素会趋向于值小的在前面,值大的在后面,达到基本有序,这将有利于我们高效率地进行最后一次全体直接插入排序

缩小增量基本思想是这样的:首先取一个整数gap < n作为间隔(经试验,普遍认为gap=(n/3)+1效率比较高)将全部元素氛围gap个子序列,所以距离gap的元素放在同一个子序列,在每一个子序列分别施行直接插入排序,然后缩小间隔gap,重复子序列划分和排序,直到gap==1,所有元素在同一个序列。由于在排序后期,大多元素已经基本有序,所以排序速度仍然很快。

代码

public class ShellSort {    public static void shellSort(int a[]){        int n = a.length;        int gap = n;        int j;        do{            gap = gap/3 + 1;//缩小间隔,直到间隔为1,进行全体直接插入排序            for (j = gap; j < n; j++){//从数组第 gap+1 个元素开始                  if (a[j] < a[j - gap])//每个元素与自己组内的数据进行直接插入排序                  {                      int temp = a[j];                      int k = j - gap;                      while (k >= 0 && a[k] > temp)                      {                          a[k + gap] = a[k];                          k -= gap;                      }                      a[k + gap] = temp;                  }             }        }while(gap > 1);    }    public static void main(String[] args) {        int[] arr = new int[]{2,7,3,0,5,4};        shellSort(arr);        for(int i = 0;i < arr.length;i++){            System.out.print(arr[i] + ",");        }    }}

快速排序


快速排序是目前最通用的高效的内部排序算法,平均时间复杂度为O(nlogn),一般情况下所需要的额外内存也是O(nlogn),如果不考虑额外内存的消耗,通常使用快排,在大量随机数排序时,其表现最好。另外,快排是一种不稳定的排序。

思路

快速排序可以采用递归调用,每一次调用能对当前所有元素进行一次划分,保证在被选中的基本元素左边都是比它小的,右边都是比它大的。

如第一次我们选择所有元素的第一个元素作为基准元素,经过一次快排后,基准元素的位置可能变化到其他地方,但是此时可以保证的是在基准元素左边的都是比它小,在右边都是比它大的元素。这样一来,我们再对其左边的所有元素进行递归调用快排,右边的所有元素递归调用快排,最终就能完成排序。

这里进行一次快排的步骤演示:示例待排序元素:【2,7,3,0,1,4】,高亮为已经排好序的元素,<表示指针left>表示指针right

  1. 初始定义两个指针:left指向数组下标0,即第一个元素;right指针数组下标5,即最后一个元素。指针left往右移动,指针right往左移动。两指针相遇,表示全部元素已被遍历,完成一次快排。
  2. 首先选择第一个元素2作为基准元素,从右往左移动指针right,找第一个比它小的元素,找到元素1,移动元素1到指针left所在位置,即数组下标0的位置。现在指针right从右往左走到数组下标4的位置。当前排序情况为:【1<,7,3,0,>1,4】。
  3. 接着从左往右找比基准元素2大的元素(下文指的从左往右或从右往左即为排序情况中<>里的元素),找到元素7,把元素7移动到指针right的位置,同时指针left向右移动一步走到数组下标1的位置。当前排序情况:【1,7<,3,0,>7,4】。
  4. 继续从右往左找比基准元素小的,找到元素0,于是把元素0移动到指针left所在位置,同时指针right向左走了一步走到数组下标3的位置。当前排序情况:【10<,3,>0,7,4】。
  5. 继续从左往右找比基准元素大的,找到元素3,于是把元素3移动到指针right所在位置,同时指针left向右走了一步走到数组小标4的位置。当前排序情况:【10,3<,>37,4】。
  6. 接着从右往左移动指针right,立即和left相遇,表示遍历结束,于是把基准元素2放到相遇的位置,即数组下标2。此时完成一次快排,基准元素2左边都比它小,右边都比它大。当前排序情况:【102<>,37,4】。
  7. 接着再对基准元素左边进行递归快排,同样右边元素也进行递归快排。

代码

public class QuickSort{    public static void quickSort(int[] arr,int start,int end){        if(start >= end)    return;        int left = start;   //当前这段数组最左端下标        int right = end;        //当前这段数组最右端下标        int standard = arr[start];      //定义当前这段数组最左端元素为基准元素        while(left < right){    //left从左递增,right从右递减相遇了,说明所有元素都扫描过了。            //从右往左找比基准值小的            while(arr[right] >= standard && left < right){                right--;            }            if(left < right){   //如果没有找到,则left = right,说明基准元素右边都比他大,则不用替换。                arr[left++] = arr[right];   //left++ 保证下一步从左往右找时从 left + 1 个找起,因为第i在本行已经被替换确认为是比基准元素小的            }            //从左往右找比基准值大的            while(arr[left] <= standard && left < right){                       left++;            }            if(left < right){                arr[right--] = arr[left];            }        }        //此时left == right这个位置的值应该为基准值,且左边所有元素比它小,右边比它大。        arr[left] = standard;        //继续递归调用        quickSort(arr, start, left-1);      //对当前基准值左边元素排序        quickSort(arr, right+1, end);       //对当前基准值右边元素排序    }    public static void main(String[] args) {        int[] arr = new int[]{2,7,3,0,5,4};        quickSort(arr,0,arr.length-1);        for(int i = 0;i < arr.length;i++){            System.out.print(arr[i] + ",");        }    }}

堆排序


堆排序也是一种高效的内部排序算法,平均时间复杂度为O(nlogn)。由于其利用这一数据结构,所以实现起来相对麻烦一些;不过只要理解了,实际上堆排序算法非常简洁。由于结构经常需要向上或向下调整,所以堆排序也是一种不稳定排序。

思路

理解堆排序的重点在于理解,这里简要介绍下

是一种特殊的完全二叉树,主要在于它的所有父节点都大于或小于子节点,所以堆也分为最大堆最小堆。由于其根节点可以一直维持为所有最大或最小的元素,这种结构称为优先队列。在一些需要操作所有元素的最大或最小值时效率特别高。

如在最大堆中取得最大值的时间复杂度为O(1);而维持最大堆的时间复杂度也仅为O(logn)

  1. 利用这一特点,我们可以每次将根节点,即堆的最大值,与倒数第(n-i)个节点交换值(这里n为节点个数,i为已交换次数),这一步只需要O(1)的时间复杂度。但此时最大堆的特性已经被破坏,根节点不再是最大值了。
  2. 这时我们再调整为最大堆,这一步耗费O(logn)
  3. 重复步骤 1,2 直到所有(n == i),即所有结点都交换成功。此时从根节点开始遍历,就是完成的升序。

所以最关键的算法就是调整堆为最大堆,做法如下:

从根节点开始,与左右子结点进行比较,如果子结点比父节点大,则两结点交换,即把较大值向上调整,保持父节点始终比子节点大。接着再把被调整下来的原父节点继续与子结点进行比较和调整。一旦父节点比左右子节点大,则不再往下调整。

(设所有结点个数为n,当前结点为第i个结点,则左子树存在公式为:(i * 2) <= n,同理右子数存在公式为:(i * 2 + 1) <= n;如果是以数组存储堆的话,该公式改为:左子树存在公式为:(i * 2 + 1) <= (n - 1),同理右子数存在公式为:(i * 2 + 2) <= (n -),其中i为数组下标。)

而这一切的前提是最大堆已经建立成功,所以下面介绍下最大堆的建立:

这里有个做法能在O(n)的时间复杂度里完成最大堆的建立。首先有个元素数量为n的数组,我们知道完全二叉树的倒数第一个非叶子节点的位置为第n/2个,在数组中的下标为n/2 - 1。把大于这个下标的节点直接作为叶子节点开始进行建堆。然后从倒数第n/2个位置开始,进行最大堆调整,这时就能把倒数第n/2个位置的节点调整为具有最大值的父节点。同理,依次对倒数第n/2 - 1个、n/2 - 2个 … 1个节点进行调整,最终就能得到整个最大堆。

代码

public class HeapSort {    private int[] heap;//用来存放堆的数组    private int count;//用来存储堆中元素的个数    //排序    public void heapSort(){        while(count > 1){                swap(0,--count);                siftdown(0);        }    }    //向下调整函数    public void siftdown(int i) //传入一个需要向下调整的结点编号i,这里传入0,即从堆的顶点开始向下调整    {        int maxIndex = 0;//记录较大值的下标        boolean needSiftDown = true;//用来标记是否需要继续向下调整        //当节点有儿子且需要向下调整        while((i*2 + 1) <= count-1 && needSiftDown)        {                    //得到节点和其左儿子中较大的一个的下标                maxIndex = (heap[i] < heap[(i*2 + 1)]) ? (i*2 + 1) : i;            if((i*2 + 2) <= count-1)//如果有右儿子,再得到当前较大的下标                    maxIndex = (heap[maxIndex] < heap[(i*2 + 2)]) ? (i*2 + 2) : maxIndex;            //如果发现最大的结点编号不是自己,说明子结点中有比父结点更大的              if(maxIndex != i)            {                swap(maxIndex,i);//交换                i = maxIndex;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整            }            else                    needSiftDown = false;//则否说明当前的父结点已经比两个子结点都要大了,不需要在进行调整了        }    }    //建堆    public void creat()    {        //从最后一个非叶结点到第1个结点依次进行向上调整        for(int i = count/2 - 1;i >= 0;i--)            siftdown(i);    }    //通过下标交换数组堆中两个元素值    public void swap(int x,int y)    {        int temp = heap[x];        heap[x] = heap[y];        heap[y] = temp;    }    //赋值时同时得到个数    public void setHeap(int[] heap) {        this.heap = heap;        this.count = heap.length;    }    public int[] getHeap() {        return heap;    }    public static void main(String[] args) {        int[] arr = new int[]{2,7,3,0,5,4};        HeapSort hs = new HeapSort();        hs.setHeap(arr);// 设置待排序的数组        hs.creat();//建堆        hs.heapSort();//排序        for(int i = 0;i < hs.getHeap().length;i++){            System.out.print(hs.getHeap()[i] + ",");        }    }}

归并排序


归并排序是一种高效内部排序算法,与快排堆排序相比它的优势在于:它是稳定的排序,并且性能与输入顺序无关,总是为O(nlogn)

思路

归并排序采用分治法,将大问题不断化为更小的问题,直到小问题达到能够直接解决的程度,再向上不断合并小问题。

如我们要对一个数组采用归并排序,我们不断地将它从中间划分成两个小数组,小数组再划分成小数组,直到小数组元素个数为1,能很方便地进行计算,就开始合并两个已经排好序的小数组,不断地向上合并,最终整个数组就是排好序的。

所以现在关键在于对两个有序数组的合并。我们可以这么做:

设有有序数组 AB;以及临时合并数组 C

首先选取AB数组中第一个元素较小的元素放入数组C中,假设拥有最小元素的数组为A数组。接着再从A数组中取第二个元素与B数组的第一个元素进行比较,谁小就放入C数组,不断重复这样的步骤,期间如果有一个数组为空,则直接把另一个数组后面元素全部放到C数组中,完成合并。

代码

public class MergeSort {    public static void sort(int a[], int first, int last, int temp[]){        if (first < last)          {              int mid = (first + last) / 2;              sort(a, first, mid, temp);    //左边有序             sort(a, mid+1, last, temp); //右边有序            merge(a, first, mid, last, temp); //再将两个有序数列合并          }      }    public static void merge(int a[], int first, int mid, int last, int temp[])      {          int i = first;        int j = mid + 1;         int k = 0;          while (i <= mid && j <= last)          {              if (a[i] <= a[j])                      temp[k++] = a[i++];              else                      temp[k++] = a[j++];           }          while (i <= mid)              temp[k++] = a[i++];          while (j <= last)              temp[k++] = a[j++];         for (i = 0; i < k; i++)  //回写            a[first + i] = temp[i];     }     public static void main(String[] args) {        int[] arr = new int[]{2,7,3,0,5,4};        int[] temp = new int[6];        sort(arr, 0, arr.length-1, temp);        for(int i = 0;i < arr.length;i++){            System.out.print(arr[i] + ",");        }    }}

参考


《数据结构》殷人昆版
白话经典算法系列之五 归并排序的实现
坐在马桶上看算法(12):堆—神奇的优先队列

0 0
原创粉丝点击