排序算法总结


本学习笔记参考了网上各个版本的排序算法的讲解,综合整理而成,仅仅作为自己的学习笔记,不少知识点未记录,

均用从小到大排序为例,排序数组为arr= { 3,6,9,4,1,5,2,7,8,0};使用C#进行编程

排序算法的稳定性:排序算法稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。
排序算法稳定性的好处。**排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。**基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。


定义数组:public static int[] arr= { 3,6,9,4,1,5,2,7,8,0};

交换方法:

        //交换方法
        public static void Swap(int[] arr,int i,int j)
        {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }

调用主函数:

        static void Main(string[] args)
        {
            RadixSort(arr,1);
            //QuickSort(arr,0,arr.Length-1);
            //HeapSort(arr);
            //MergeSort(arr);
            //ShellSort(arr);
            //InsertionSortDichotomy(arr);
            //InsertionSort(arr);
            //SelectionSort(arr);
            //CocktailSort(arr);
            //BubbleSort(arr);
            for (int i = 0; i < arr.Length; i++)
            {
                Console.WriteLine(arr[i]);
            }
            Console.ReadLine();
        }

冒泡排序

重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。

        public static void BubbleSort(int[] arr)
        {
            for (int i = 0; i < arr.Length-1; i++)
            {
                for (int j = 0; j < arr.Length-1-i; j++)
                {
                    if (arr[j]>arr[j+1]) //如果改成arr[j]>=arr[j+1]则变为不稳定排序
                    {
                        Swap(arr,j,j+1);
                    }
                }
            }
        }

如图所示:


鸡尾酒排序

鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。也是一种稳定排序

        public static void CocktailSort(int[] arr)
        {
            int left = 0;                           //初始化边界
            int right = arr.Length - 1;
            while (left<right)
            {
                for (int i = left; i < right; i++)  //前半轮,将最大元素放到后面
                {
                    if (arr[i]>arr[i+1])
                    {
                        Swap(arr,i,i+1);
                    }
                }
                right--;
                for (int i = right; i > left; i--)  //后半轮,将最小元素放到前面
                {
                    if (arr[i-1]>arr[i])
                    {
                        Swap(arr, i-1, i);
                    }
                }
                left++;
            }
        }

如图所示:


选择排序

工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。是一种不稳定的排序。

        public static void SelectionSort(int[] arr)
        {
            for (int i = 0; i < arr.Length-1; i++)      // 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, min, i);                  // 放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法
                }
            }
        }

如图所示:
选择排序图例 宏观过程


插入排序

插入排序是的工作原理非常类似于我们抓扑克牌。对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
插入排序不适合对于数据量比较大的排序应用。
直接插入排序代码:

        public static void InsertionSort(int[] arr)
        {
            for (int i = 1; i < arr.Length; i++)
            {
                int get = arr[i];                       //右手抓取一张扑克牌
                int j = i - 1;
                while (j >= 0 && arr[j] > get)          //将抓到的牌与手牌从右向左进行比较
                {
                    arr[j + 1] = arr[j];                //如果该手牌比抓到的牌大,就将其右移
                    j--;
                }
                arr[j + 1] = get;                       //直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
            }
        }

图例:


二分插入排序

如果插入排序的比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的次数,我们称为二分插入排序。

        public static void InsertionSortDichotomy(int[] arr)
        {
            for (int i = 1; i < arr.Length; i++)
            {
                int get = arr[i];                       // 右手抓到一张扑克牌
                int left = 0;                           // 拿在左手上的牌总是排序好的,所以可以用二分法
                int right = i - 1;                      // 手牌左右边界进行初始化
                while (left <= right)                   // 采用二分法定位新牌的位置
                {
                    int mid = (left + right) / 2;
                    if (arr[mid] > get)
                        right = mid - 1;
                    else
                        left = mid + 1;
                }
                for (int j = i - 1; j >= left; j--)     // 将欲插入新牌位置右边的牌整体向右移动一个单位
                {
                    arr[j + 1] = arr[j];
                }
                arr[left] = get;                        // 将抓到的牌插入手牌
            }
        }

图示:
二分插入排序


希尔排序

希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率,但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

示例代码:

        public static void ShellSort(int[] arr)
        {
            int h = 0;						
            while (h <= arr.Length)                    // 生成初始增量(步长)
            {
                h = 3 * h + 1;
            }
            while (h >= 1)
            {
                for (int i = h; i < arr.Length; i++)
                {
                    int j = i - h;
                    int get = arr[i];
                    while (j >= 0 && arr[j] > get)
                    {
                        arr[j + h] = arr[j];
                        j = j - h;
                    }
                    arr[j + h] = get;
                }
                h = (h - 1) / 3;                    // 递减增量
            }
        }

图例:


归并排序

该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序列的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序若将两个有序表合并成一个有序表,称为2-路归并。
数据量比较大的时候,归并排序的效率比较高,仅次于快速排序。归并排序是一种稳定的排序算法
算法思路:
1、把长度为n的输入序列分成两个长度为n/2的子序列;
2、对这两个子序列分别采用归并排序;
3、将两个排序好的子序列合并成一个最终的排序序列。
归并排序代码:

        public static void MergeSort(int[] array)
        {
            MergeSort(array, 0, array.Length - 1);
        }

        private static void MergeSort(int[] array, int p, int r)
        {
            if (p < r)
            {
                int q = (p + r) / 2;
                MergeSort(array, p, q);
                MergeSort(array, q + 1, r);
                Merge(array, p, q, r);
            }
        }

        private static void Merge(int[] array, int p, int q, int r)
        {
            int[] L = new int[q - p + 2];
            int[] R = new int[r - q + 1];
            L[q - p + 1] = int.MaxValue;
            R[r - q] = int.MaxValue;

            for (int i = 0; i < q - p + 1; i++)
            {
                L[i] = array[p + i];
            }

            for (int i = 0; i < r - q; i++)
            {
                R[i] = array[q + 1 + i];
            }

            int j = 0;
            int k = 0;
            for (int i = 0; i < r - p + 1; i++)
            {
                if (L[j] <= R[k])
                {
                    array[p + i] = L[j];
                    j++;
                }
                else
                {
                    array[p + i] = R[k];
                    k++;
                }
            }
        }

图例:
在这里插入图片描述


堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序是不稳定的排序算法,不稳定发生在堆顶元素与arr[i]交换的时刻。

        private static void Heapify(int[] arr, int i, int size)  // 从arr[i]向下进行堆调整
        {
            int left_child = 2 * i + 1;         // 左孩子索引
            int right_child = 2 * i + 2;        // 右孩子索引
            int max = i;                        // 选出当前结点与其左右孩子三者之中的最大值
            if (left_child < size && arr[left_child] > arr[max])
                max = left_child;
            if (right_child < size && arr[right_child] > arr[max])
                max = right_child;
            if (max != i)
            {
                Swap(arr, i, max);                // 把当前结点和它的最大(直接)子节点进行交换
                Heapify(arr, max, size);          // 递归调用,继续从当前结点向下进行堆调整
            }
        }
        private static int BuildHeap(int[] arr, int n)           // 建堆,时间复杂度O(n)
        {
            int heap_size = n;
            for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
                Heapify(arr, i, heap_size);
            return heap_size;
        }
        private static void HeapSort(int[] arr)
        {
            int heap_size = BuildHeap(arr, arr.Length);    // 建立一个最大堆
            while (heap_size > 1)           // 堆(无序区)元素个数大于1,未完成排序
            {
                // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
                // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
                Swap(arr, 0, --heap_size);
                Heapify(arr, 0, heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
            }
        }

图例:


快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。
步骤为:
1、从序列中挑出一个元素,作为"基准"(pivot).
2、把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
3、对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

        private static void Heapify(int[] arr, int i, int size)  // 从arr[i]向下进行堆调整
        {
            int left_child = 2 * i + 1;         // 左孩子索引
            int right_child = 2 * i + 2;        // 右孩子索引
            int max = i;                        // 选出当前结点与其左右孩子三者之中的最大值
            if (left_child < size && arr[left_child] > arr[max])
                max = left_child;
            if (right_child < size && arr[right_child] > arr[max])
                max = right_child;
            if (max != i)
            {
                Swap(arr, i, max);                // 把当前结点和它的最大(直接)子节点进行交换
                Heapify(arr, max, size);          // 递归调用,继续从当前结点向下进行堆调整
            }
        }
        private static int BuildHeap(int[] arr, int n)           // 建堆,时间复杂度O(n)
        {
            int heap_size = n;
            for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
                Heapify(arr, i, heap_size);
            return heap_size;
        }
        private static void HeapSort(int[] arr)
        {
            int heap_size = BuildHeap(arr, arr.Length);    // 建立一个最大堆
            while (heap_size > 1)           // 堆(无序区)元素个数大于1,未完成排序
            {
                // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
                // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
                Swap(arr, 0, --heap_size);
                Heapify(arr, 0, heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
            }
        }

        //快速排序
        private static int Partition(int[] arr, int left, int right)  // 划分函数
        {
            int pivot = arr[right];               // 这里每次都选择最后一个元素作为基准
            int tail = left - 1;                // tail为小于基准的子数组最后一个元素的索引
            for (int i = left; i < right; i++)  // 遍历基准以外的其他元素
            {
                if (arr[i] <= pivot)              // 把小于等于基准的元素放到前一个子数组末尾
                {
                    Swap(arr, ++tail, i);
                }
            }
            Swap(arr, tail + 1, right);           // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
                                                // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
            return tail + 1;                    // 返回基准的索引
        }
        private static void QuickSort(int[] arr, int left, int right)
        {
            if (left >= right)
                return;
            int pivot_index = Partition(arr, left, right); // 基准的索引
            QuickSort(arr, left, pivot_index - 1);
            QuickSort(arr, pivot_index + 1, right);
        }

图例:

快速排序

基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
算法描述:
1、取得数组中的最大数,并取得位数;
2、arr为原始数组,从最低位开始取每个位组成radix数组;
3、对radix进行计数排序(利用计数排序适用于小范围数的特点);

        public static int[] RadixSort(int[] arr, int maxDigit)//输入要排序的数组和数字的最大位数
        {
            for (int i = 0; i < maxDigit; i++)
            {
                int[] tempArray = new int[arr.Length];
                int[] countingArray = new int[10];
                // 初始化计数数组
                for (int j = 0; j < 10; j++)
                {
                    countingArray[j] = 0;
                }

                // 元素计数
                for (int j = 0; j < arr.Length; j++)
                {
                    int splitNum = (int)(arr[j] / Math.Pow(10, i)) - (int)(arr[j] / Math.Pow(10, i + 1)) * 10;
                    countingArray[splitNum] = countingArray[splitNum] + 1;
                }
                // 计数小与等于某元素的个数
                for (int j = 1; j < 10; j++)
                {
                    countingArray[j] += countingArray[j - 1];
                }

                for (int j = arr.Length - 1; j >= 0; j--)
                {
                    int splitNum = (int)(arr[j] / Math.Pow(10, i)) - (int)(arr[j] / Math.Pow(10, i + 1)) * 10;
                    int splitNumIndex = countingArray[splitNum] - 1;
                    tempArray[splitNumIndex] = arr[j];

                    countingArray[splitNum] -= 1;
                }

                Array.Copy(tempArray, arr, tempArray.Length);
            }

            return arr;//返回排序好的数组
        }

基数排序图示:


总结
排序算法对比表

排序算法参照表


感悟及思考

学了这么多,其实以上各种排序算法在大部分实际应用中我们可能都用不到,实际上我们直接调用内部封装好的排序算法就行,比如:
Array.Sort(arr);
Array.Reverse(arr);
在这里需要提的是
一、比代码的具体实现更重要的是理解各个排序算法的特性以及算法实现的主要思想。比如我们需要理解我们所需要排序的字段、数组、列表的特点从而选择是否需要用到稳定排序。稳定排序和非稳定排序适用于哪些场景。
二、稳定排序在一些已经按照既定规则排好序的同时再按照新的规则进行再排序的时候应用比较广泛。比如大量人员名单列表时,要将已经按照首字母排好序的名单进行考评等级的再排序。
 非稳定排序,如一些简单的数字数组则无需考虑到相同元素的交换问题
三、对于排序算法实现的思想,如堆排序中的二叉堆,归并排序和快速排序的分治策略,基数排序的优先级思想都是非常值得细细研究的。
四、没有所谓最优的排序算法,只有最适用的排序算法,我们要根据实际的数据情况选择适合的排序算法,如,对于只有简单的几个数字组成的列表,我会毫不犹豫地使用冒泡排序,写起来简单快速又稳定。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值