数据结构(六)——排序

一:排序的概述

排序可以看成是线性表的一种操作。排序是一个在现实生活中经常要遇到的问题,例如考试分数的排名(分数从大到小排列)、做操时的排队(身高从矮到高排列)
在计算机中的排序问题上,我们通常把数据元素称为记录

——排序的稳定性
当对a1,a2,a3三个待排记录进行降序排序,a1=a3,若排序后a1仍在a3前面,则称为稳定排序,若排序后a3排在a1前面则称为不稳定排序


——内排序与外排序
根据在排序过程中待排序的所有记录是否全部被放置在内存中,分为内排序和外排序
外排序是由于排序的记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间进行多次数据交换才能进行
内排序是将排序的记录全部放置在内存中进行
一般只讨论内排序的一些方法,内排序分为插入排序、交换排序、选择排序和归并排序


二:最简单的排序算法

基本思想:将每个记录依次与它后面的记录进行比较,如果反序则交换

for (int i = 0; i < array.Length - 1; i++)
{
    for (int j = i + 1; j < array.Length; j++)
    {
        if (array[j] < array[i])
        {
            int temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }
}

外循环每趟循环完就能找到一个最小值,但是这个算法每趟循环的作用就只能找到一个最小值,对于其他位置并没有任何帮助,并且这个算法时间复杂度最好最坏情况下的次数都是O(n^2),也就是说,这个算法是非常低效的!


三:冒泡排序——交换排序

冒泡排序就像炒股,不断的买进卖出进行交换,想通过价格差来实现盈利
基本思想:比较相邻的两两记录,如果反序则交换,直到没有反序记录为止。共有两种思路——每次找到最大记录或每次找到最小记录
因为每趟排序都将最大或最小记录冒出来,因此叫作冒泡排序



在这里插入图片描述

//每次找到最小的记录
for (int i = 1; i <= array.Length - 1; i++)
{
    for (int j = array.Length - 1; j >= i; j--)
    {
        if (array[j] < array[j - 1])
        {
            int temp = array[j];
            array[j] = array[j - 1];
            array[j - 1] = temp;
        }
    }
}

//每次找到最大的记录
for (int i = 1; i <= array.Length - 1; i++)
{
    for (int j = 0; j < array.Length - i; j++)
    {
        if (array[j] > array[j + 1])
        {
            int temp = array[j];
            array[j] = array[j + 1];
            array[j + 1] = temp;
        }
    }
}

外循环是趟数(n-1趟),内循环是本趟依次比较的两个记录的下标,每次找到最终的表尾元素或最终的表头记录
以上两种写法的时间复杂度最好最坏情况都是O(n^2),如果对于{2,1,3,4,5,6,7,8,9}的待排序列,第一趟交换完之后数列就已经有序了,但是算法仍然继续进行比较,很没有意义。当某一趟排序没有任何数据交换时,则说明序列已经有序了,所以对于冒泡排序有一种优化的写法

bool isSort = true;
for (int i = 1; i <= array.Length-1 && isSort; i++)
{
    isSort = false;
    for (int j = 0; j < array.Length - i; j++)
    {
        if (array[j] > array[j + 1])
        {
            int temp = array[j];
            array[j] = array[j + 1];
            array[j + 1] = temp;
            isSort = true;
        }
    }
}

优化之后最好情况下时间复杂度是O(n),最坏情况下时间复杂度是O(n^2)


四:简单选择排序——选择排序

简单选择排序就像炒股,很少出手,等到时机成熟再买进或卖出进行交换
基本思想:它不像冒泡排序一样进行多次的记录交换,而是每次找到一个最小的记录再进行交换
因为每趟排序并不是不断的交换记录,而是每趟遍历完之后选择一个最小的记录进行交换,因此叫作简单选择排序


在这里插入图片描述

int minIndex;
for (int i = 0; i < array.Length - 1; i++)
{
    minIndex = i;
    for (int j = i + 1; j < array.Length; j++)
    {
        if (array[j] < array[minIndex])
        {
            minIndex = j;
        }
    }
    if (minIndex != i)
    {
        int temp = array[minIndex];
        array[minIndex] = array[i];
        array[i] = temp;
    }
}

简单选择排序的最大特点就是交换记录的次数相当少,它的时间复杂度与冒泡排序相同,同为O(n^2),但是因为记录的交换次数少,所以在性能上还是略优于冒泡排序

扫描二维码关注公众号,回复: 8513907 查看本文章

五:直接插入排序——插入排序

直接插入排序就像平时玩牌时的摸牌,依次把小的插入到左边,大的插入到右边
基本思想:每趟都得到一串有序记录(第一趟有序记录就是下标为0位置的记录,它的下一个记录作为待排记录),每趟将待排记录插入到这串有序记录的适合位置,直到全部插入完为止

在这里插入图片描述

//i=1的写法(标准写法)
for (int i = 1; i < array.Length; i++)
{
    if (array[i] < array[i - 1])
    {
        int temp = array[i];
        int j;
        for (j = i; j > 0 && array[j - 1] > temp; j--)
        {
            array[j] = array[j - 1];
        }
        array[j] = temp;
    }
}


//i=0的写法
for (int i = 0; i < array.Length - 1; i++)
{
    if (array[i + 1] < array[i])
    {
        int temp = array[i + 1];
        int j;
        for (j = i + 1; j > 0 && array[j - 1] > temp; j--)
        {
            array[j] = array[j - 1];
        }
        array[j] = temp;
    }
}

直接插入排序算法在最好情况下的时间复杂度是O(n),最坏情况下的时间复杂度是O(n^2),它在性能上比冒泡排序和简单选择排序要好一些


引言:以上三种排序算法(冒泡排序,简单选择排序,直接插入排序)的时间复杂度都是都是O(n^2),下面几种排序算法都属于上面算法的改进版,让时间复杂度突破了O(n^2)的界限


六:希尔排序——插入排序

基本思想:按照增量进行分组,对每组记录采用直接插入排序的方法进行排序

在这里插入图片描述

int increment = array.Length;
while (increment > 1)
{
    increment /= 2;
    for (int i = 0; i < increment; i++)
    {
        for (int j = i + increment; j < array.Length; j += increment)
        {
            if (array[j] < array[j - increment])
            {
                int temp = array[j];
                int k;
                for (k = j - increment; k >= 0 && array[k] > temp; k -= increment)
                {
                    array[k + increment] = array[k];
                }
                array[k + increment] = temp;
            }
        }
    }
}

例如第六种排序算法直接插入排序例子中的最后一趟循环,2进行了7次交换才插入到了适合的位置,假设有100个记录,2同样排在最后一个,那么要进行99次交换才插入到适合的位置,那样的话岂不是很低效,这是因为直接插入排序每次只能够移动一个记录的位置,对后续的待排记录不会产生任何影响,而希尔排序可以进行跳跃式的移动,每次while循环后会将序列不断的排列为基本有序,当增量为1时,进行序列整体的直接插入排序
希尔排序的关键是增量值的选取,但是目前还没有人找到一种最好的增量序列,一般每次折半就好
希尔排序算法的时间复杂度是O(n^3/2),突破了O(n^2)的慢速排序,更加高效了,但是它是一种不稳定排序


七:堆排序——选择排序


堆排序不适合排序序列个数较少的情况,堆排序算法的时间复杂度是O(nlogn),但是它是一种不稳定排序


八:归并排序——归并排序

在这里插入图片描述
归并排序算法的时间复杂度是O(nlogn)


九:快速排序——交换排序

思想:分治法,通过一趟排序将待排记录分割成独立的两个部分,其中一部分的关键字均比另一部分记录的关键字小,再对这两部分继续进行递归排序,直到各部分中只有一个数
快速排序基本上都内置在了软件的开发工具包中,例如C#中的Array.Sort()底层实现就是用的快速排序算法

using System;

class Program
{
    static int[] array = { 5, 6, 9, 8, 3, 7, 4, 1, 2 };
    static void Main(string[] args)
    {
        Sort(0, 8);
    }

    //快速排序的递归调用函数
    static void Sort(int low, int high)
    {
        if (low < high)
        {
            int pivot = Partition(low, high);
            Sort(low, pivot - 1);
            Sort(pivot + 1, high);
        }
    }

    //将每次的待排序列分割为两部分
    static int Partition(int low, int high)
    {
        //*****优化部分(三数取中)—使枢纽数pivot的选取更合理
        int m = low + (high - low) / 2;
        if (array[high] < array[low])
        {
            int temp = array[high];
            array[high] = array[low];
            array[low] = temp;
        }
        if (array[m] > array[high])
        {
            int temp = array[m];
            array[m] = array[high];
            array[high] = temp;
        }
        if (array[m] < array[low])
        {
            int temp = array[m];
            array[m] = array[low];
            array[low] = temp;
        }
        //*****

        int key = array[low];
        while (low < high)
        {
            while (low < high && array[high] >= key)
            {
                high--;
            }
            array[low] = array[high];
            while (low < high && array[low] <= key)
            {
                low++;
            }
            array[high] = array[low];
        }
        array[low] = key;
        return low;
    }
}

以上代码中Partition函数尤为重要,就是每次选取不同的枢纽数pivot进行分割,不断的使每一段待排序列中的记录都满足小于pivot的数在pivot的左边,大于的在pivot的右边
快速排序不适合排序序列个数较少的情况,它是目前综合性能最佳的排序算法,快速排序算法的时间复杂度是O(nlogn),但是它是一种不稳定排序


十:总结

简单算法:冒泡排序、简单选择排序、直接插入排序
改进算法:快速排序、堆排序、希尔排序、归并排序

快速排序是性能最好的排序算法,但是对于不同的情况我们也应该考虑使用不用的算法应对

发布了127 篇原创文章 · 获赞 278 · 访问量 24万+

猜你喜欢

转载自blog.csdn.net/LLLLL__/article/details/100973308