先来一张盗图

n: 数据规模
k:“桶”的个数
In-place: 占用常数内存,不占用额外内存
Out-place: 占用额外内存
稳定性:排序后2个相等键值的顺序和排序之前它们的顺序相同

冒泡排序

冒泡排序须知:

作为最简单的排序算法之一,冒泡排序给我的感觉就像Abandon在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。。。冒泡排序还有一种优化算法,就是立一个flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。。。

什么时候最快(Best Cases):

当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊。。。。)

什么时候最慢(Worst Cases):

当输入的数据是反序时(写一个for循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗。。。)

冒泡排序动图演示

static void Main(string[] args)
        {
            int temp = 0;
            int[] arr = {23, 44, 66, 76, 98, 11, 3, 9, 7};
            for (int i = 0; i < arr.Length - 1; i++)
            {
                #region将大的数字移到数组的arr.Length-1-i
                for (int j = 0; j < arr.Length - 1 - i; j++)
                {
                    if (arr[j] > arr[j + 1])
                    {
                        temp = arr[j + 1];
                        arr[j + 1] = arr[j];
                        arr[j] = temp;
                    }
                }
            }
            Console.WriteLine("排序后的数组:");
            foreach (int item in arr)
            {
                Console.Write(item+"");
            }
            Console.WriteLine();
            Console.ReadKey();
        }

选择排序(Selection Sort)

选择排序须知:

在时间复杂度上表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

选择排序不稳定的原因就是因为在交换的过程中 相同值可能被交换位置 5 6 5 2 3   对这组进行排序的时候  5 2 交换 但是 55 位置变化了

选择排序动图演示

/// <summary>
        /// 02选择排序
        /// </summary>
        /// <param name="array"></param>
        /// <returns></returns>
        public static int[] SelectionSort(int[] array)
        {
            int temp = 0;
            for (int i = 0; i < array.Length - 1; i++)
            {
                int minVal = array[i]; //假设 i 下标就是最小的数
                int minIndex = i;
                for (int j = i + 1; j < array.Length; j++)
                {
                    if (minVal > array[j])
                    {
                        minVal = array[j];
                        minIndex = j;
                    }
                }
                temp = array[i];
                array[i] = array[minIndex];
                array[minIndex] = temp;

                printArray(array);
                Console.WriteLine("第" + (i + 1) + "趟");
            }
            return array;
        }

插入排序(Insertion Sort)

插入排序

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法步骤:

1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

算法分析:

  平均时间复杂度:O(n²)

  空间复杂度:O(1)  (用于记录需要插入的数据)

  稳定性:稳定

public static void Main(string[] args)
        {
            int[] arrNumber = new int[] { 3, 2, 1, 4, 5, 6 };
            InsertSort(arrNumber);
            foreach (int i in arrNumber)
            {
                Console.WriteLine(i);
            }
            Console.ReadKey();
        }
        public static void InsertSort(int[] arrNumber)
        {
            for (int i = 1; i < arrNumber.Length; i++)
            {
                int temp = arrNumber[i];
                int j;
                for (j = i; j>0&&arrNumber[j-1]>temp; j--)
                {
                    arrNumber[j] = arrNumber[j - 1]; 
                }
                arrNumber[j] = temp;
            }
        }

希尔排序(Shell Sort)

希尔排序算法是将数组的所有元素按照一定增量d分组,对每组内的数据实行插入排序,之后不断减小增量,每组内所包含的元素也越多,增量减少至1时,整个数组被分成一组,插入排序结束后整个数组的排序便完成。

 

操作步骤:

初始时,有一个大小为 10 的无序序列。

(1)在第一趟排序中,令增量d = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。

(2)按照直接插入排序的方法对每个组进行排序。

(3)在第二趟排序中,我们把上次的 d 缩小一半,即 d= d / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。

(4)按照直接插入排序的方法对每个组进行排序。

(5)在第三趟排序中,再次把 d 缩小一半,即d = d / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。

(6)按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。

public void shellsort(int[]a)
        {
            int d = a.Length / 2;
            while(d>=1)
            {
                for(int i=0;i<d;i++)
                {
                    for(int j=i+d;j<a.Length;j+=d)
                    {
                        int temp=a[j];//存储和其比较的上一个a[x];
                        int loc = j;
                        while (loc - d >= i&&temp < a[loc - d])//&&j-d>=i
                        {
                            a[loc] = a[loc - d];
                            loc = loc - d;
                        }
                        a[loc] = temp;
                    }
                }
                //一次插入排序结束,缩小d的值
                d = d / 2;
            }
这里d(gap)动态生成
function shellSort(arr) { var len = arr.length, temp, gap = 1; while(gap < len/3) { //动态定义间隔序列 gap =gap*3+1; } for (gap; gap > 0; gap = Math.floor(gap/3)) { for (var i = gap; i < len; i++) { temp = arr[i]; for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) { arr[j+gap] = arr[j]; } arr[j+gap] = temp; } } return arr; }

归并排序(Merge Sort)

归并排序须知:

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

 1、 自底向上的方法
(1) 自底向上的基本思想
     自底向上的基本思想是:第1趟归并排序时,将待排序的文件R[1..n]看作是n个长度为1的有序子文件,将这些子文件两两归并,若n为偶数,则得到n/2个长度为2的有序子文件;若n为奇数,则最后一个子文件轮空(不参与归并)。故本趟归并完成后,前n/2 - 1个有序子文件长度为2,但最后一个子文件长度仍为1;第2趟归并则是将第1趟归并所得到的n/2个有序的子文件两两归并,如此反复,直到最后得到一个长度为n的有序文件为止。
     上述的每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其为"二路归并排序"。类似地有k(k>2)路归并排序。   

 

2、自顶向下的方法(本文主要介绍此种方法,下面的文字都是对此种方法的解读)

(1) 自顶向下的基本思想
     采用分治法进行自顶向下的算法设计,形式更为简洁。
     自顶向下的归并排序:是利用递归和分而治之的技术将数据序列划分成为越来越小的半子表,再对半子表排序,最后再用递归步骤将排好序的半子表合并成为越来越大的有序序列,归并排序包括两个步骤,分别为:

      1)划分子表

      2)合并半子表


(1)分治法的三个步骤
     设归并排序的当前区间是R[low..high],分治法的三个步骤是:
①分解:将当前区间一分为二,即求分裂点
②求解:递归地对两个子区间R[low..mid]和R[mid+1..high]进行归并排序;
③组合:将已排序的两个子区间R[low..mid]和R[mid+1..high]归并为一个有序的区间R[low..high]。
  递归的终结条件:子区间长度为1(一个记录自然有序)。

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。

归并

//归并排序(目标数组,子表的起始位置,子表的终止位置)
        private static void MergeSortFunction(int[] array, int first, int last)
        {
            try
            {
                if (first < last)   //子表的长度大于1,则进入下面的递归处理
                {
                    int mid = (first + last) / 2;   //子表划分的位置
                    MergeSortFunction(array, first, mid);   //对划分出来的左侧子表进行递归划分
                    MergeSortFunction(array, mid + 1, last);    //对划分出来的右侧子表进行递归划分
                    MergeSortCore(array, first, mid, last); //对左右子表进行有序的整合(归并排序的核心部分)
                }
            }
            catch (Exception ex)
            { }
        }

        //归并排序的核心部分:将两个有序的左右子表(以mid区分),合并成一个有序的表
        private static void MergeSortCore(int[] array, int first, int mid, int last)
        {
            try
            {
                int indexA = first; //左侧子表的起始位置
                int indexB = mid + 1;   //右侧子表的起始位置
                int[] temp = new int[last + 1]; //声明数组(暂存左右子表的所有有序数列):长度等于左右子表的长度之和。
                int tempIndex = 0;
                while (indexA <= mid && indexB <= last) //进行左右子表的遍历,如果其中有一个子表遍历完,则跳出循环
                {
                    if (array[indexA] <= array[indexB]) //此时左子表的数 <= 右子表的数
                    {
                        temp[tempIndex++] = array[indexA++];    //将左子表的数放入暂存数组中,遍历左子表下标++
                    }
                    else//此时左子表的数 > 右子表的数
                    {
                        temp[tempIndex++] = array[indexB++];    //将右子表的数放入暂存数组中,遍历右子表下标++
                    }
                }
                //有一侧子表遍历完后,跳出循环,将另外一侧子表剩下的数一次放入暂存数组中(有序)
                while (indexA <= mid)
                {
                    temp[tempIndex++] = array[indexA++];
                }
                while (indexB <= last)
                {
                    temp[tempIndex++] = array[indexB++];
                }

                //将暂存数组中有序的数列写入目标数组的制定位置,使进行归并的数组段有序
                tempIndex = 0;
                for (int i = first; i <= last; i++)
                {
                    array[i] = temp[tempIndex++];
                }
            }
            catch (Exception ex)
            { }
        }

快速排序(Quick Sort)

快速排序须知:

又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高! 它是处理大数据最快的排序算法之一了。虽然Worst Case的时间复杂度达到了O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为O(n log n) 的排序算法表现要更好,可是这是为什么呢,我也不知道。。。好在我的强迫症又犯了,查了N多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:(快速排序的最坏运行情况是O(n²),比如说顺序数列的快排。但它的平摊期望时间是O(n log n) ,且O(n log n)记号中隐含的常数因子很小,比复杂度稳定等于O(n log n)的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。)

快速排序

首先,对于一大堆无序的数字,从中随机挑选一个(通常都是第一个,或者最后一个),比如是53,这个被选上的数字被称为枢值/基准数(枢纽的枢),接下来,然后设立两个指针 i 和 j  ,左边的为 i,右边的为 j ,然后将两个指针分别移动,左边的向右移动,右边的向左移动(如果基准数是第一个,那就先从右边开始比对,如果基准数是最后一个,那就先从第一个比对) 在移动的过程中,将每一个元素分别与基准数比对,左边的比对过程中如果比基准数小,不变,反之,交换(和原先空位置交换,因为在比对之前,将一个数拿出来作为基准数,所以作为基准数的那个位置就空了),右边的比对过程中如果比基准数大,不变,反之,交换,这样将所有要排序的数字分成两部分,一部分是大于等于枢值53的,一部分是小于枢值53的。在第一步完成后,一大堆无序的数字就变得稍微有序一点了。
第二步,从上面得到的两堆数字,分别采用第一步的方法各自再找一个枢值。对于第一堆,由于所有的数字都比53大,至少也等于53,因此,第二次随机挑选的枢值肯定是一个大于53的数字,比如79;类似地,对于第二堆,由于所有的数字都小于53,因此第二次随机挑选的枢值肯定小于它,比如4。接下来,再把两堆数字各自分成大于等于相应枢值的数字序列,以及小于枢值的数字序列。这样做下来,原来的一大堆数就变成了四小堆,它们分别是小于4的数字,介于4到53之间的,介于53到79之间的,以及大于或等于79的。
再接下来,用同样的方法,四堆变八堆,八堆变十六堆,很快所有的数字就排好序了。

个人理解:在上图中,有一列无序数字,是2,4,5,1,3    要对它进行快速排序,让它变成有序数列

第一步

先找出基准数为 “2” ,也就是数列的第一个元素(这是时2 的位置就空了,就相当有一排人,要对他们的身高进行排序,先让第一个人站到最前边,原先位置空出来)

第二步

设立两个指针分别为 left 和 right (最左边和最右边)

第三步

先移动 right 指针(因为基准数是第一个,这个位置是空的)向左移动,与基准数进行比对,如果比基准数大则不变,继续移动,如果比基准数小,则交换到原先基准数的位置(也就是第一个位置)第一个进行比对的是 “3” 、比基准数大 不变,接着移动到 “1”  、1 比基准数小,交换,这时原先基准数的位置的元素就是“1”,这个时候数列就是:1  4  5  1(这个1已经没有了,交换到第一个基准数位置了)  3

第四步

再移动left指针(这个时候原先基准数的位置的元素就是从右边交换过来的首个比基准数小的元素 “1”)与基准数比对,指针从 元素“1” 移动到元素“4”   4比基准数大,则交换到原先元素“1”的位置

第五步

不断重复三和四步骤,不停的移动left和right,当指针left和right和重合的时候,就将原先被抽出来的基准从放入到里边。这时,一个无序的数列就变得有序了,左边的都是比基准数“2”小的元素,而右边都是比基准数“2”大的元素!

第六步

以原先基准数“2”为分割点,分别采用上边步骤进行排序,经过递归过程,最后排序结束(在程序里递归就是自己调用自己,不停重复,直到满足条件才能结束)

class Counts
    {
        public static int count1 = 0;
        public static int count2 = 0;
    }
    class Program
    {
        static void Main(string[] args)
        {
            int[] array = new int[20];
            Random rand = new Random();
            for (int i = 0; i < 20; i++)
            {
                array[i] = rand.Next(0, 100);
            }
            Console.WriteLine("排序前的数组:");
            foreach (var item in array)
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine();
            Console.WriteLine("冒泡排序后的数组:");
            foreach (var item in Sort.PopSort(array))
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine("\n快速排序后的数组:");
            int[] arr = new int[array.Length];
            array.CopyTo(arr, 0);
            Sort.QuickSort(arr, 0, arr.Length - 1);
            foreach (var item in arr)
            {
                Console.Write("{0} ", item);
            }
            Console.WriteLine("\n\n冒泡排序的比较次数:{0}\n快速排序的交换次数:{1}", Counts.count1, Counts.count2);
            Console.ReadLine();
        }
    }

    static class Sort
    {
        /// <summary>
        /// 冒泡排序
        /// </summary>
        /// <param name="array"></param>
        /// <returns></returns>
        public static int[] PopSort(int[] array)
        {
            int[] arr = new int[array.Length];
            array.CopyTo(arr, 0);
            for (int i = 0; i < arr.Length; i++)
            {
                for (int j = 0; j < arr.Length - 1; j++)
                {
                    if (arr[j] > arr[j + 1])
                    {
                        int temp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = temp;
                        Counts.count1++;
                    }
                    else continue;
                }
            }
            return arr;
        }
        /// <summary>
        /// 快速排序
        /// </summary>
        /// <param name="array"></param>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <returns></returns>
        private static int sortUnit(int[] array, int left, int right)
        {
            //以left为基准数,数组第一个元素
            int key = array[left];
            while (left < right)
            {
                //因为基准数是第一个元素,所有从右边开始进行比对
                while (array[right] >= key && right > left)
                {
                    //比基准数大,不变,继续向左移动指针
                    --right;
                }
                array[left] = array[right];
                Counts.count2++;
                while (array[left] <= key && right > left)
                {
                    //比基准数小,不变,继续向右移动指针
                    ++left;
                }
                array[right] = array[left];
                Counts.count2++;
            }
            //最后将基准数放到指针重合位置
            array[left] = key;
            return right;
        }
       /// <summary>
       /// 快速排序
       /// </summary>
       /// <param name="array">要排序的无序数组</param>
       /// <param name="left">指针left</param>
       /// <param name="right">指针right</param>
        public static void QuickSort(int[] array, int left, int right)
        {
            //重合就return  因为左边的要小于右边的
            if (left >= right)
            {
                return;
            }
            //调用快速排序方法,将数组和指针传入
            int index = sortUnit(array, left, right);
            //递归调用,分别对排序好的左右两小部分(左边小于原先基准数的数列 右边大于原先基准数的数列)进行重复步骤排序
            QuickSort(array, left, index - 1);//左边
            QuickSort(array, index + 1, right);//右边
        }
    }

 

堆排序(Heap Sort)

堆排序须知:

堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列

堆排序

 posted on 2019-07-30 17:27  JetaimeBeaucoup  阅读(150)  评论(0编辑  收藏  举报