数据结构与算法——从零开始学习(七)排序算法

系列文章

第一章:基础知识

第二章:线性表

第三章:栈和队列 

第四章:字符串和数组

第五章:树和二叉树

第六章:图


第七章 排序

(附Android源码)本章案例demo下载:http://download.csdn.net/download/csdn_aiyang/9943795 

一、基础概念

二、简单排序(插入、冒泡、选择)

2.1 插入排序

2.2 冒泡排序

2.3 选择排序

三、希尔排序

四、快速排序

五、堆排序

六、归并排序


一、基础概念

排序(Sorting)是计算机程序设计中的一种重要操作,其功能是将一个数据元素集合或序列重新排列成一个按数据元素某个项值有序的序列。作为排序依据的数据项被称为“排序码”,也即数据元素的关键码。按关键码对元素序列进行排序,若相同关键码值的元素间关系在排序前与排序后保持一致,称此排序方法是稳定排序,否则为不稳定排序。例如,a[i]=a[j],且a[i]在 a[j]之前,在排序后,a[i]仍在 a[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

排序的性能评价标准不外乎排序过程的时间和空间代价,时间代价是以排序过程中数据元素的关键码之间的比较次数和数据元素运动次数来反映,空间代价是以排序过程中需要的附近空间量来表示。

此外,排序分为内部排序和外部排序,即元素量大不大是不是在内存中进行排序,一般都是采用内排序。

 

二、简单排序(插入、冒泡、选择)

2.1 插入排序

插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

数据结构与算法——从零开始学习(七)排序算法_第1张图片

(1)逻辑步骤:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置中,重复步骤2。

(2)算法如下:

     //直接插入排序
    private void InsertSort(int[] a) {
   
        for (int i = 1; i < a.length; i++) {
            //待插入的 新元素temp
            int temp = a[i];

            int j;
            for (j = i - 1; j >= 0; j--) {  
                //将大于temp的往后移动一位
                if (a[j] > temp) {
                    a[j + 1] = a[j];
                }
            }
            //循环结束后,temp是最小的值,由于循环最后会j--,所以放到j+1位置。
            a[j + 1] = temp;
        }
    }

(3)小结:插入排序算法由嵌套的两个循环组成。外层循环为n-1次,内层循环for的逻辑稍复杂一些。算法最佳时间复杂度为O(n)。最差情况出现在每个记录进入for循环都必须比较到子序的最前端,子序中每个记录都必须移动,待插记录方可插入,算法的时间复杂度为O(n²)。因为附近空间只需一个监测点temp,所以算法的空间复杂度是O(1)。该算法是稳定排序。

 

2.2 冒泡排序

冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)非常简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

(1)逻辑步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样操作,从开始第一对到结尾的最后一对。在这一点,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

(2)算法代码:

 //冒泡
    private void pubbleSort(int[] numbers) {
        int temp;//记录临时变量
        int size = numbers.length;//数组大小

        for (int i = 0; i < size - 1; i++) {
                temp = numbers[i];
            for (int j = 0; j < size; j++) {
                if (numbers[i] > numbers[j]) {//小的放前面             
                  
                    numbers[i] = numbers[j];
                    numbers[j] = temp;
                }
                    
            }
        }
    }

(3)排序过程: 

第一趟排序示意图:

数据结构与算法——从零开始学习(七)排序算法_第2张图片

冒泡整个过程:

数据结构与算法——从零开始学习(七)排序算法_第3张图片

(4)小结:空间上只用了一个辅助单元temp。时间复杂度为O(n²)。

 

2.3 选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。

(1)逻辑步骤:第一趟,从n个记录中找出关键码最小的记录与第一个记录交换;第二趟,从第二个记录开始的n-1个记录中再挑选出关键码最小的记录与第二个记录交换;如此,第i趟,则从第i个记录开始的n-i+1个记录中选出关键码最小的记录与第i个记录交换,直到整个序列按关键码有序。

(2)排序过程示意图:

数据结构与算法——从零开始学习(七)排序算法_第4张图片

(3)算法代码:

//选择排序
    public void selectSort(int[] array) {
        int min;
        int size = array.length;

        for (int i = 0; i < size; i++) {
            min = array[i];//设最小为数组第一索引的值
            for (int j = i+1; j < size; j++) {
                if (array[j] < min) {  
                    min = array[j];//找到更小值,拿到索引
                }
            }
            array[j] = array[i];//替换
            array[i] = min;
        }
    }

(4)小结:选择排序移动记录次数较少,但关键码的比较次数依然是n(n+1)/2次,所以时间复杂度为O(n²)。

 

三、希尔排序

希尔(Shell,外壳)排序,也称递减增量排序算法,是插入排序的一种高速而稳定的改进版本。基于插入排序的以下两点性质而提出改进方法的:

 

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  2. 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位。

(1)逻辑思想:对待排序的记录序列先“宏观”再“微观”调整。即先将待排序记录序列分割成若干个“较稀疏的”子序列,分别进行直接插入排序,最后对全部记录进行一次直接插入排序。

(2)算法代码:

//希尔排序
    private void HeerSort(int[] a) {
        int d = a.length / 2;
        while (true) {
            for (int i = 0; i < d; i++) {
                for (int j = i; j + d < a.length; j += d) {
                    int temp;
                    if (a[j] > a[j + d]) {
                        temp = a[j];
                        a[j] = a[j + d];
                        a[j + d] = temp;
                    }
                }
            }
            if (d == 1) {
                break;
            }
            d--;
        }
    }

(3)小结:希尔排序的分析是一个复杂的问题,因为它的时间耗损是所取的“增量”序列的函数。到目前为止,尚未有人求得一种最好的增量序列,但大量研究也得出了一些局部的结论:在排序的过程中,相同关键字记录的领先关系发生变化,则说明该排序方法是不稳定的。例如待排序序列{2,4,1,2},采用希尔排序,设d1=2,则得到一趟排序结果{1,2,2,4},将最后一个2放到了第二个位置。说明希尔排序法是不稳定的排序算法

 

四、快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。也暂且被称为“最优”算法,在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。

(1)算法步骤:

从数列中挑出一个元素,称为 "基准"(pivot),重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

(2)算法代码:

  //Main 方法执行入口
    public static void main(String[] args) {
        int[] a ={3,5,1,4,6,2};
        quickSort(a,0,a.length-1);
    }

  /**
     * 快速排序,递归排序:选取第一个索引的值为标记值最后放到中间位置
     * @param a 数组——待排序队列
     * @param low —— 开始位置
     * @param high —— 结束位置
     */
    public static void quickSort(int[] a , int low,int high) {
        int start = low;
        int end = high;
        int temp = a[start];//标记值

        while (start < end){
            //通行条件:while()中的值为停止循环条件,即标记值更小就停止循环
            while (start < end && temp <= a[end]){
                end--;
            }
            //再次确认标记值是否大于后面的值
            if (temp > a[end]){
                a[start] = a[end];
                a[end] =temp;
            }

            while (start < end && temp >= a[start]){//标记值更大就停止循环
                start++;
            }
            if (temp < a[start]){
                a[end] = a[start];
                a[start] = temp;
            }
        }
       //当start >= end 时将跳出循环   
       //此时temp值一定会在数组的中间 即(a =2,1,3,4,6,5,), 左边比3小,右边比3大 

        if(start-1 > low ){
            quickSort(a,low,start-1);
        }

        if(end+1

打印结果:

初始化:a = [3, 5, 1, 4, 6, 2]
快排一次结果a--------[2, 1, 3, 4, 6, 5]
递归左
快排二次结果a--------[1, 2, 3, 4, 6, 5]
递归右
快排三次结果a--------[1, 2, 3, 4, 6, 5]
递归右
快排四次结果a--------[1, 2, 3, 4, 5, 6]

五、堆排序

堆积排序(Heap sort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

public static void heapSort(int[] arr)
{
    buildHeap(arr);
    for (int tail=arr.length-1; tail>=1; tail--) {
        swap(arr, 0, tail);
        heapify(arr, 0, tail);   // 此时 tail 恰好是剩余堆的 size
    }
}

private static void buildHeap(int[] arr)
{
    for (int i=arr.length/2; i>=0; i--) {
        heapify(arr, i, arr.length);
    }
}

private static void heapify(int[] arr, int parent, int size)
{
    int left = parent*2 + 1;
    int right = parent*2 + 2;
    int maxIndex = parent;
    if (leftarr[maxIndex]) {
        maxIndex = left;
    }
    if (rightarr[maxIndex]) {
        maxIndex = right;
    }
    if (maxIndex != parent) {
        swap(arr, parent, maxIndex);
        heapify(arr, maxIndex, size);
    }
}

private static void swap(int[] arr, int i, int j)
{
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

六、归并排序

归并排序(Merge sort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法,是1945年有约翰逊·冯·诺依曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。时间复杂度:最好、最坏、平均均是O(nlogn),稳定排序。

(1)递归方式

核心思想是:先分后归。先拆分为一个个有序的子序列,再将一个个有序子序列进行归并操作,最后合并为一个有序的序列。

缺点:简单易懂,但是会造成时间、空间(需额外的O(logn))上的损耗。

为什么递归方式会造成时间、空间的损耗?

因为递归使用了调用自身函数的方式,在 JVM 中方法函数就对应的是一个栈帧,对函数的操作除了函数自身内部逻辑实现外,还包括栈帧的入栈和出栈。其中栈帧是会占用额外的空间,入栈、出栈操作会占用一定的时间。

原理图: 

数据结构与算法——从零开始学习(七)排序算法_第5张图片

public static void mergeSortRecur(int[] arr, int left, int right) 
{
    if (left == right) {
        return;
    }
    int mid = (left + right) / 2;
    mergeSortRecur(arr, left, mid);
    mergeSortRecur(arr, mid+1, right);
    merge(arr, left, mid, right);
}

public static void mergeSortIter(int[] arr, int len) 
{
    int left, mid, right;
    for (int i=1; i

优化后:

    /**
     * 归并排序
     * @param unSorted 待排序序列
     */
    private static void mergeSort(int[] unSorted){
        int len = unSorted == null ? 0 : unSorted.length;
        //打印排序前的序列
        printArray(unSorted);

        if(len > 1){
            //先建一个等长于原数组的临时数组,避免递归中频繁开辟空间
            int[] sorted = new int[len];
            mergeSort(unSorted, 0, len - 1, sorted);
        }

        //打印排序后的序列
        printArray(unSorted);
    }
   /**
     * 打印数组
     * @param arrays
     */
    private static void printArray(int[] arrays){
        int len = arrays == null ? 0 : arrays.length;
        for(int i = 0; i < len; i++){
            System.out.print(arrays[i] + " ");
        }
    }

    /**
     * 归并排序
     * @param unSorted 待排序序列
     * @param left 序列的起始位置
     * @param right 序列的结束位置
     * @param sorted 两个已排序序列合并后存放的临时序列
     */
    private static void mergeSort(int[] unSorted, int left, int right, int[] sorted){
        if(left >= right){
            return;
        }
        int mid = (left + right) / 2;
        //左递归归并排序,使得左序列有序
        mergeSort(unSorted, left, mid, sorted);
        //右递归归并排序,使得右序列有序
        mergeSort(unSorted, mid + 1, right, sorted);
        //将两个有序序列归并操作
        merge(unSorted, left, mid, right, sorted);
    }

    /**
     * 归并操作
     * @param unSorted 待排序序列
     * @param left 第一个序列的起始位置
     * @param mid 第二个序列的起始位置
     * @param right 序列的结束位置
     * @param sorted 两个已排序序列合并后存放的临时序列
     */
    private static void merge(int[] unSorted, int left, int mid, int right, int[] sorted){
        int i = left; // 左序列的起始下标
        int j = mid + 1; //右序列的起始下标
        int t = 0; // 临时下标,表示合并后存放的位置
        while(i <= mid && j <= right){
            if(unSorted[i] < unSorted[j]){
                sorted[t++] = unSorted[i++];
            }else{
                sorted[t++] = unSorted[j++];
            }
        }
        //将左序列剩余的元素填充如临时序列
        if(i <= mid){
            int len = mid - i + 1;
            System.arraycopy(unSorted, i, sorted, t, len);
            t += len;
        }
        //将右序列剩余的元素填充如临时序列
        if(j <= right){
            int len = right - j + 1;
            System.arraycopy(unSorted, j, sorted, t, len);
            t += len;
        }
        //将临时序列中元素拷贝到原序列
        System.arraycopy(sorted, 0, unSorted, left, t);
    }

(2)迭代方式 

实现上相对递归比较没那么清晰易懂,但是不会像递归方式造成额外的时间、空间损耗。

原理:
1、将序列的每相邻两个元素进行归并操作,形成ceil(n/2)个序列,排序后每个序列包含一个或两个元素
2、若此时序列数不是1,那么将上面的序列进行归并,形成ceil(n/4)个序列,排序后每个序列包含三个或者四个元素
3、重复步骤2,知道排序完成,即序列上为1

public static void startInteratorMergeSort(){
        //随机生成待排序的序列
        int[] unSorted = makeRandomArray(0, 1000, 10);

        iteratorMergeSort(unSorted);

    }

    public static void iteratorMergeSort(int[] arr) {
        int len = arr == null ? 0 : arr.length;
        if(len < 2){
            return;
        }
        //建一个等长于原数组的临时数组,用于存放合并后的序列
        int[] orderedArr = new int[len];
        for (int i = 2; i < len * 2; i *= 2) {
            for (int j = 0; j < (len + i - 1) / i; j++) {
                // 左序列起始位置
                int left = i * j;
                // 左序列结束位置、右序列的起始位置
                int mid = left + i / 2 >= len ? (len - 1) : (left + i / 2);
                // 右序列结束位置
                int right = i * (j + 1) - 1 >= len ? (len - 1) : (i * (j + 1) - 1);
                // 用于存放合并后的序列的起始下标
                int start = left;
                // 左序列的起始下标
                int l = left;
                // 左序列的结束下标、右序列的起始下标
                int m = mid;
                while (l < mid && m <= right) {
                    if (arr[l] < arr[m]) {
                        orderedArr[start++] = arr[l++];
                    } else {
                        orderedArr[start++] = arr[m++];
                    }
                }

                //将左序列剩余的元素填充如临时序列
                int mergeLen = mid - l;
                if(l < mid){
                    System.arraycopy(arr, l, orderedArr, start, mergeLen);
                    start += len;
                }
                //将右序列剩余的元素填充如临时序列
                mergeLen = right - m + 1;
                if(m <= right){
                    System.arraycopy(arr, m, orderedArr, start, mergeLen);
                    start += len;
                }
                mergeLen = right - left + 1;
                //将临时序列中元素拷贝到原序列
                System.arraycopy(orderedArr, left, arr, left, mergeLen);
            }
        }
    }

七、总结

首先,从算法的平均时间复杂度、最坏时间复杂度和算法所需的辅助控件三个方面,对各种排序方法进行比较如下:

数据结构与算法——从零开始学习(七)排序算法_第6张图片

其次,从排序方法的稳定性角度对各种排序方法加以比较。插入排序、冒泡排序、归并排序是稳定的,而选择排序、快速排序、堆排序是不稳定的。

排序算法在计算机程序设计中非常重要,根据上面比较的各种排序方法的特点,根据上面比较的各种排序方法的特点,其适用的场合也不同。在选择哪种排序方法时需要考虑如下因素:待排序的记录数目n的大小;记录本身数据量的大小,也就是记录中除关键码外的其他信息量的大小;关键码的结构及其分布情况;对排序稳定性的要求。依据这些条件,可得出结论如下:

(1)若数目n较小(n<50),可采用插入、冒泡、选择排序。如果数目较多且移动费时,应采用选择排序;

(2)若记录的初始状态依据按关键码基本有序,则选用直接插入或冒泡排序法。

(3)若数目n较大,则应采用快速排序、堆排序、归并排序法。这三个时间复杂度一样,但就平均性能而言,快速排序被认为是目前基于比较记录关键码的内部排序中最好的排序方法,但遗憾的是,快速排序在最坏情况下的时间复杂度是O(n²)(情况不常见),堆排序与归并排序在最坏情况下的时间复杂度仍保持不变。堆排序和快速排序法都是不稳定的,若要求稳则选归并。

(4)前面讨论的排序算法都是顺序存储实现的。当记录本身的信息量很大时,为避免大量时间用在移动数据上,可以用链表作为存储结构。插入、归并都容易在链表上实现。

综上所述,每一种排序方法各有特点,没有绝对最优的,应根据具体情况选择合适的排序方法,当然也可以结合使用。

 

流行算法排序实用案例demo下载地址:

http://download.csdn.net/download/csdn_aiyang/9943795 

 

你可能感兴趣的:(算法结构+Java基础)