常用排序算法分析

版权声明:本文为博主原创文章,未经博主允许不得转载。技术交流可邮:[email protected] https://blog.csdn.net/cjh94520/article/details/38805951
常用排序算法分析

  • 常用排序算法有冒泡排序,选择排序,插入排序,堆排,希尔排序和快排;
  • 本文尽可能详尽地分析每一种排序算法的定义,实现代码,算法时间复杂度和稳定性。

1.冒泡排序(BubbleSort)
  • 定义:冒泡排序,是模拟气泡上升过程的排序。气泡在上升过程体积不断变大,相似与排序比较中,相邻的两个“气泡”比较大小,大的“气泡”换到后面,一次排序后最大的“气泡”已经到达最后面,这样下次排序就可以不用再处理,只处理剩余的“气泡”,重复N-1次排序(要排序的数量N),完成排序。
  • 实现算法(C#):
     
              
    static void BubbleSort(int[] array)
    {
    int n = array.Length; //数组的长度
    for (int i = 0; i < n - 1; i++)
    for (int j = 0; j < n - 1 - i; j++)
    if (array[j] > array[j + 1])
    {
    int temp = array[j];
    array[j] = array[j + 1];
    array[j + 1] = temp;
    }
    }

  • 时间复杂度:第一个for循环执行了n次,第二个for循环每次执行(n-1-i)次,一共n次,总共是  n*(n-1-i)次,根据时间复杂度算法,取最高次幂去系数,O(n2)
  • 稳定性:由于算法中比较大小冒泡排序就是把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
2.选择排序(SelectionSort)
  • 定义:首先在未排序序列中找到最小元素,记录索引值,偏离完毕,将索引值所指元素与第一个元素交换,然后,再从剩余未排序元素中继续寻找最小元素,记录索引值,取索引值所指元素与第二个元素交换。以此类推,直到所有元素均排序完毕。
如序列 56,12, 80, 91, 20

第2趟:12 20 80 91 56

第3趟:12 20 56 91 80

第4趟:12 20 56 80 91

  • 实现算法(C#):
  •         static void SelectionSort(int[] array)
            {
                int n = array.Length;  //数组的长度
                int min_index  ;       //记录遍历数组时最小值的下标
    
                for (int i = 0; i < n - 1; i++)
                {
                    min_index = i;
    
                    for (int j = i + 1; j <= n - 1; j++)
                    {
                        if (array[j] < array[min_index])
                            min_index = j;
                    }
    
                    if (i != min_index)   //说明存在比i所指元素更小的元素,否则i==min_index
                    {
                        int temp = array[i];
                        array[i] = array[min_index];
                        array[min_index] = temp;
                    }
                }
            }
  • 时间复杂度:两个for循环,第一个for循环执行了n次,第二个for循环,每次执行n-1-i次,循环n次,一共进行了(n-1+n-2+...+1)次,根据      时间复杂度算法,取最高次幂去系数,O(n2).
  • 稳定性:假设遇到相同元素,由于在前面的元素先被获取,比较过程中相同元素是不会交换的。所以相同元素的前后顺序并没有改变,所以选择排序是一种稳定排序算法.

3.插入排序(Insert)
  • 定义:插入即表示将一个新的数据插入到一个有序数组中,并继续保持有序。例如有一个长度为N的无序数组,进行N-1次的插入即能完成排序;第一次,数组第1个数认为是有序的数组,将数组第二个元素插入仅有1个有序的数组中;第二次,数组前两个元素组成有序的数组,将数组第三个元素插入由两个元素构成的有序数组中......第N-1次,数组前N-1个元素组成有序的数组,将数组的第N个元素插入由N-1个元素构成的有序数组中,则完成了整个插入排序
  • 实现代码(C#):
  •  
        static void InsertSort(int[] array)
        {
            int n = array.Length;    //数组长度

            for (int i = 1; i < n; i++)
            {
                int insert_num = array[i];
                int j;
                for (j = i; j > 0 && array[j-1] > insert_num ; j--)
                {
                    array[j] = array[j-1];   //从排序好的序列后面开始比较,将比插入元素大的元素向后挪
                }
                //循环最后一次赋值剩余array[j-1],经过自减后 j 就是最后一个未赋值元素的坐标
                    array[j] = insert_num;   
            }
        }

  • 时间复杂度:如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n2
  • 稳定性:插入元素是从原先有序的序列后面开始比较,当遇到相同的元素时,也只会把新元素插入到相同元素的后面,不会影响两者的先后顺序,所以插入排序是稳定的算法。

4.希尔排序(ShellSort)
  • 定义:希尔排序是插入排序的一种。是针对插入排序算法的改进。该方法又称缩小增量排序。先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1,即所有记录放在同一组中进行直接插入排序为止。
  • 例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:
  • 13 14 94 33 82
    25 59 94 65 23
    45 27 73 25 39
    10

    然后我们对每列(每一竖,因为刚好到第5个)进行排序:

    10 14 73 25 23
    13 27 94 33 39
    25 59 94 65 82
    45
    

    将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].

    这时10已经移至正确位置了,然后再以3为步长进行排序:

    10 14 73
    25 23 13
    27 94 33
    39 25 59
    94 65 82
    45
    

    排序之后变为:

    10 14 13
    25 23 33
    27 25 59
    39 65 73
    45 94 82
    94
    

    最后以1步长进行排序(此时就是简单的插入排序了)。

  • 实现代码:
  •         static void ShellSort(int[] array)
            {
                int n = array.Length;  //数组长度
    
                for (int k = n / 2; k >= 1; k/=2)
                {
                    for (int i = k; i < n; i += k)
                    {
                        int insert_num = array[i];
                        int j;
                        for (j = i; j > 0 && array[j - k] > insert_num; j -= k)
                        {
                            array[j] = array[j - k];
                        }
                        //循环最后一次赋值剩余array[j-1],经过 j-=k 后就是最后一个未赋值元素的坐标
                        array[j] = insert_num;
                    }
                }
            }
  • 时间复杂度:平均时间复杂度:希尔排序的时间复杂度和其增量序列有关系,这涉及到数学上尚未解决的难题(我承认自己不懂=_=);不过在某些序列中复杂度可以为O(n^1.3)。
  • 稳定性:由于增量的原因,序列中元素并不是相邻比较,所以会出现不稳定的现象。举个简单的栗子;3,1,1,4,初始增量为2(数量的一半),分成3,1 和 1,4两组,各自排序再组合结果就会变成1,1,3,4


5.快速排序(QuickSort)
  • 定义快速排序是对冒泡排序的一种本质改进。它的基本思想是通过一趟扫描后,使得排序序列的长度能大幅度地减少。在冒泡排序中,一次扫描只能确保最大数值的数移到正确位置,而待排序序列的长度可能只减少1。快速排序通过一趟扫描,就能确保某个数(以它为基准点吧)的左边各数都比它小,右边各数都比它大。然后又用同样的方法处理它左右两边的数(分治法),直到基准点的左右只有一个元素为止。
  • 实现代码
  •         static void QuickSort(int[] array, int left, int right)
            {
                int tag;
                if (left < right)
                {
                    tag = Partition(array, left, right);
                    QuickSort(array, left, tag - 1);
                    QuickSort(array, tag + 1, right);
                }
            }
    
            static int Partition(int[] array, int left, int right)
            {
                int temp = array[left];
    
                while (left < right)
                {
                    while (left < right && array[right] >= temp)   //直到找到比基值小的数
                        right--;
    
                    if (left < right)
                    { array[left] = array[right];}
    
                    while (left < right && array[left] <= temp)     //直到找到比基值大的数
                        left++;
    
                    if (left < right)
                    {  array[right] = array[left];}
                }
                array[left] = temp;
                return left;
            }

  • 时间复杂度:假设有1到8代表要排序的数,快速排序会递归log(8)=3次,每次对n个数进行一次处理,所以他的时间复杂度为n*log(n)。
  • 稳定性:由于算法中从后面开始找比基数小的数,存在不稳定的现象。在元素交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3  3  6, 现在基数元素5和3交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在元素交换的时刻


5.堆排序(HeapSort)
  • 定义:堆分为最大堆和最小堆,其实就是完全二叉树。最大堆要求节点的元素都要大于其孩子,最小堆要求节点元素都小于其左右孩子,两者对左右孩子的大小关系不做任何要求有了上面的定义,我们可以得知,处于最大堆的根节点的元素一定是这个堆中的最大值。其实我们的堆排序算法就是抓住了堆的这一特点,每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最大堆,依次类推,最终得到排序的序列。或者说,堆排序将所有的待排序数据分为两部分,无序区和有序区。无序区也就是前面的最大堆数据,有序区是每次将堆顶元素放到最后排列而成的序列。每一次堆排序过程都是有序区元素个数增加,无序区元素个数减少的过程。当无序区元素个数为1时,堆排序就完成了。
  • 实现代码:
  •         static void HeapSort(int[] array)
            {
                //序列的长度
                int length = array.Length;
    
                //先建立一个堆
                BuildHeap(array,length);
    
                // 取堆顶和最后面的元素交换,剔除堆顶,剩余元素继续成堆,直到剔除所有元素
                while (length > 0)
                {
                    int temp = array[0];
                    array[0] = array[length - 1];
                    array[length - 1] = temp;
    
                    length--;  //排除堆顶出去
    
                    AdjustHeap(array,length, 0);
                }
            }
    
            static void BuildHeap(int[] array, int length)
            {
                int index = length / 2 - 1;
                while (index>=0)
                {
                    AdjustHeap(array, length, index);
                    index--;
                }
            }
    
            static void AdjustHeap(int[] array, int length, int index)
            {
                int left  = index * 2 + 1;             //左孩子坐标
                int right = index * 2 + 2;         //右孩子坐标
                int largest = index;                 //父节点坐标
    
                while (left < length || right < length)
                {
                    if (left < length && array[largest] < array[left])
                    {
                        largest = left;                 //记录当前较大值的坐标
                    }
                    if (right < length && array[largest] < array[right])
                    {
                        largest = right;               //记录当前较大值的坐标
                    }
                    if (largest != index)
                    {
                        //交换数据
                        int temp = array[index];
                        array[index] = array[largest];
                        array[largest] = temp;
    
                        index = largest;    
                        left  = index * 2 + 1 ;
                        right = index * 2 + 2 ;
                    }
                    else
                    {
                        break;
                    }
                }
            }
  • 时间复杂度:在用数组的实现方式中,每次都把新加入的数值放入数组的末尾。如果这个数比它现在所在位置的父节点的数值小,那么就要把他俩交换。如果在新的位置上还是比它的父节点小,就递归地继续这个过程。这个过程和二叉树的高度成正比,所以是logn。如果把所有n个数都插入进来,那么最坏情况的时间就是O(n*logn)
  • 稳定性:堆排序是不稳定的:比如:3 27 36 27,如果堆顶3先输出,则,第三层的27(最后一个27)跑到堆顶,然后堆稳定,继续输出堆顶,是刚才那个27,这样说明后面的27先于第二个位置的27输出,不稳定.


6.总结

排序法

最差时间分析 平均时间复杂度 稳定度 空间复杂度
冒泡排序 O(n2) O(n2) 稳定 O(1)
快速排序 O(n2) O(n*log2n) 不稳定 O(log2n)~O(n)
选择排序 O(n2) O(n2) 稳定 O(1)

插入排序

O(n2) O(n2) 稳定 O(1)
堆排序 O(n*log2n) O(n*log2n) 不稳定 O(1)
希尔排序 —— —— 不稳定 O(1)
  


猜你喜欢

转载自blog.csdn.net/cjh94520/article/details/38805951