0
点赞
收藏
分享

微信扫一扫

算法基础(一):常见排序算法

笙烛 2022-01-31 阅读 82
算法

记忆口诀: 选泡插,快归堆兮桶计基。恩方恩老恩要散,对恩加K恩乘K。不稳稳稳不稳稳,不稳不稳稳稳稳

怎么调试bug?:  通读程序 -> 输出中间值 -> 剪功能 。 核心是定位

选择排序:

选择排序不稳定是因为有横跨交换。 最符合人类思维的排序方式,从第一个位置开始到最后一个位置,每个位置都要和后面的数比较一次,如果后面有更小的,就拿过来。

//选择排序
            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[j];
                        array[j] = array[i];
                        array[i] = temp;
                    }
                }
            }

冒泡排序:

与选择排序不同的是,首先,比较对象是“每每相邻”的元素;其次,最优的时间复杂度为O(n).

 //冒泡排序 
            bool swapHappend = false;
            for (int i = array.Length - 1; i > 0; i--)
            {
                swapHappend = false;//每次冒泡开始前都需要将默认值重置。
                for (int j = 0; j < i; j++)
                {
                    if (array[j] > array[j + 1])
                    {
                        int temp = array[j + 1];
                        array[j + 1] = array[j];
                        array[j] = temp;
                        swapHappend = true;
                    }
                }
                if (!swapHappend)  //当一次都没交换过,证明是已经排好的
                {                  //此时时间复杂度可以达到最优:O(n)
                    return;
                }
            }

插入排序:

对于基本有序的数组最好用,稳定。性能优于冒泡排序。插入排序的数组在排序过程中,一段是有序的(如: 1, 2, 3, 4, 5, 9, 7, 11, 56,  42, 33),无序端头的元素的跟有序端进行比较,然后寻找出这个无序元素的在有序端的位置然后插入(上述数组就是元素7插入到5与9之间),插入排序算法的优势成因就是:找到第一个可插入的位置之后便可以停止循环。

 //插入排序
            for (int i = 1; i < array.Length; i++)  //从第二个位置开始往前遍历
            {
                for (int j = i; j > 0; j--)
                {
                    //由于数组前面部分是有序的,所以遇到第一个不可插入的位置
                    //就可以停止比较了
                    if (array[j - 1] > array[j])
                    {
                        int temp = array[j - 1];
                        array[j - 1] = array[j];
                        array[j] = temp;
                    }
                    else
                    {
                        break;
                    }
                }
            }

希尔排序:

插入排序的改进版,利用分组思想。相对于插入排序,首先,需要注意的是多了一个循环,该循环用于生成间隔数组;其次,应该注意分组的实现主要是依靠for循环的条件设置,边界编写容易出错。

 //Shell排序
            //计算 Knuth 序列作为间隔
            int h = 1;
            while (h < array.Length / 3)
            {
                h = 3 * h + 1;
            }
            //开始Shell排序
            for (int gap = h; gap > 0; gap = (gap - 1) / 3)
            {
                for (int i = gap; i < array.Length; i++)
                {
                    for (int j = i; j > gap - 1; j -= gap)
                    {
                        if (array[j] < array[j - gap])
                        {
                            int tempNum = array[j - gap];
                            array[j - gap] = array[j];
                            array[j] = tempNum;
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }

归并排序:

使用了递归,递归都需要考虑一个bottom,即递归调用到最后一层的情况。

 //归并排序函数
        static void mergeSort(int[] array, int aHead, int aTail)
        {
            /*参数说明
             arayOutput:原数组    aHead:数组头指针
             aTail:数组尾指针     aMiddle:分割为两个数组的标记指针
             */
            if (aHead == aTail) { return; }         //只有一个元素时,则返回

            int i = (aHead + aTail) / 2; //原数组遍历指针
            int j = i + 1;               //原数组遍历指针
            int[] arayOutputCopy = new int[aTail - aHead + 1];  //工作空间 
            int k = 0;                                          //工作空间遍历指针

            mergeSort(array, aHead, i);
            mergeSort(array, j, aTail);

            if (array[i] <= array[j]) { return; } //当数组有序,则返回

            //选择较小的元素放入工作空间
            while (aHead <= i && j <= aTail)
            {
                if (array[aHead] <= array[j])
                {
                    arayOutputCopy[k++] = array[aHead++];
                }
                else
                {
                    arayOutputCopy[k++] = array[j++];
                }
            }

            //未遍历到的数据进行一次遍历
            while (aHead <= i)
            {  arayOutputCopy[k++] = array[aHead++]; }
            while (j <= aTail)
            {  arayOutputCopy[k++] = array[j++];  }

            //最后结果赋值给原数组
            for (int l = arayOutputCopy.Length - 1; l >= 0; l--, aTail--)
            {  array[aTail] = arayOutputCopy[l];  }

        }

快速排序:

容易出bug ,需要验证极端情况,包括用于判定比较的数在数组的边界、重复数、只有两个数的情况。对于快排的理解,参考博客简单快速排序_闲来之笔-CSDN博客_简单快速排序 ,这篇博客中 “投石 -> 确定脏数据 ->移动”对于记忆该算法有帮助,这里的快速排序我第一次见是在严蔚敏版c语言数据结构那本教材中。除了“投石法”实现单轴快排,也有其他的实现方式。


        static void QuickSortIterative(int[] array, int Low, int High) 
        {
            if (Low < High) 
            {
                int pivot = QuickSort(array, Low, High);
                QuickSortIterative(array, Low, pivot-1);
                QuickSortIterative(array, pivot+1, High);
            } 
        }

        static int QuickSort(int[] array, int Low, int High) 
        { 
            int pivot = array[Low];

            while (Low < High)  
            {
                while (Low < High && array[High] >= pivot ) 
                { --High; }//循环结束说明在左侧(HIGH)找到了右侧数,或者遍历完毕
                array[Low] = array[High];

                while (Low < High && array[Low] <= pivot) 
                { ++Low; } //循环结束说明在右侧(LOW)找到了左侧数,或者遍历完毕
                array[High] = array[Low];
            } 

            array[High] = pivot; 

            return Low; 
        }

双轴快速排序(DualPivotSort):

该算法是荷兰国旗问题的解法 ,双轴快排中有三个指针,其中两个指针为慢指针,用于标记两个轴(pivot)的位置,将数组分为了三个区域;一个快指针,快指针则是用于遍历数组,每次遍历一个元素,确定应该在在三个区域中的哪一块,不容的区域执行代码不同,受快指针的移动方向影响。


        public static void DualPivodSortIterative(int[] array, int low, int high) 
        {
            if (low < high) {
                int[] pos = DualPivodSort(array, low, high);
                DualPivodSortIterative(array, low, pos[0]-1);
                DualPivodSortIterative(array, pos[0] + 1, pos[1] - 1);
                DualPivodSortIterative(array, pos[1] + 1, high);
            }
        }
        public static int[] DualPivodSort(int[] array, int low, int high) 
        {
            //使 第一个轴 <= 第二个轴
            if (array[low] > array[high]) { swapArray(array, low, high); }

            //记录轴的位置
            int pivot1 = low;
            int pivot2 = high;

            //工作指针 middle
            int middle = low + 1;

            //有三种情况,因此if有三个分支,遍历方向为 low -> high ,因此第二if不需要 middle++
            while (middle < high) 
            {
                if (array[middle] < array[pivot1]) {
                    swapArray(array, ++low, middle++);
                }
                else if (array[middle] > array[pivot2]) {
                    swapArray(array,--high,middle );
                }
                else { middle++; }
            }

            //遍历完后,让轴回到轴位,使轴左边小于轴,轴右边大于轴
            swapArray(array,pivot1,low);
            swapArray(array,pivot2,high);
              
            int[] pos = { low, high };
            return pos;
        }

 双轴快排序的改进:

双轴快速排序_闲来之笔-CSDN博客_双轴快排  ,对原本的三种情况进行了判定改进。

......
            while (middle < high)
            {
                if (array[middle] < array[pivot1])
                {
                    swapArray(array, ++low, middle);
                }
                else if (array[middle] > array[pivot2])
                {
                    high--;
                    if (array[high] < array[low])
                    {
                        low++;
                        swapArray(array, high, middle);
                        swapArray(array, middle, low);
                        while (low + 1 < high && array[low + 1] < array[pivot1]) {
                            middle = ++low;
                        }
                    }
                    else if (array[pivot1] <= array[high] && array[high] <= array[pivot2]) {
                        swapArray(array, middle, high);
                    }
                    while (high - 1 >= middle && array[high - 1] > array[pivot2]) {
                        high--;
                    }
                }
                middle++; 
            }
......

堆排序:

堆排序需要前置知识储备:“完全二叉树的数组存储方式”、“由完全二叉树总结点数 得出 非叶子结点数 、叶子结点数”

        //排序函数,先构建大顶堆,然后循环“摘顶->构建新堆”过程
        static void HeapSort(int[] array)
        {
            //大顶堆构建,从底层的最后一个非叶结点向根节点遍历
            //遍历顺序以及起始点不能变化,否则需要重写 HeapConstruct 函数
            for (int i = array.Length / 2 - 1; i > -1; i--)
            {
                HeapConstruct(array, i, array.Length);
            }
            //进行排序,即“摘顶过程”,每取一次根节点(结点最大值)与最后一个位置的结点
            //进行交换后,我们便确定了一个结点的排序位置,树的结点总数就会减1
            //摘顶完成后,树节点总数减少了,大顶堆也受到破坏,因此需要从根节点开始往后
            //遍历重新使二叉树变成大顶堆
            for (int j = array.Length - 1; j > 0; j--)
            {
                swapArray(array, j, 0);
                HeapConstruct(array, 0, j);
            }

        }

        //构建大顶堆,输入参数分别为:存储二叉树的数组、起始节点、节点总数
        static void HeapConstruct(int[] array, int node,int nodeNum)
        { 
            int pointer = node;         //遍历指针 

            //当前指针结点为"非叶子结点"则执行循环
            while (pointer < nodeNum / 2) 
            { 
                int tempPointer = pointer;  //当前指针备份
                int sonL = pointer * 2 + 1; //当前指针结点左孩子(完全二叉树的非叶结点,左孩子必然存在)
                int sonR = pointer * 2 + 2; //当前指针结点右孩子(不一定存在)

                //右孩子存在,且为较大数值
                if (sonR < nodeNum && array[sonL] < array[sonR]){  
                    pointer = sonR;
                }else { //只存在左孩子,或者左孩子为较大数值
                    pointer = sonL;
                }

                //子结点的数值更大则交换,否则终止遍历
                if (array[tempPointer] < array[pointer]) {  
                    swapArray(array, tempPointer, pointer);
                }else {
                    break;
                }
            } 
        }

计数排序:

适用于数据量大但是数据范围小。如大型企业的数万员工年龄排序、快速得知高考名次等。

属于非比较类排序。

先确定这组 数据范围 为 0~k,数据量为 n 。分配 k +1个位置 , 用于 统计 0 至  k 在这组数据当中出现的次数

然后可以简化计数排序:使其空间复杂度为(k), 但是这样会使得排序算法变为不稳定

计数排序会用到 累加数组 ,累加数组中包含了两个信息:数出现次数 ,数的位置信息。此时可以使计数排序具有稳定性,且其空间复杂度不可避免的为(n+k)

 //计数排序
        //需要已知这组数的 min,max
        static void CountSort(int[] array, int min, int max)
        {
            int k = max - min + 1;          // min~max 一共多少个数
            int[] countArray = new int[k];  // 计数数组 
            int[] sortedArray = new int[array.Length]; //辅助数组

            //遍历原序列 , 生成计数数组(包含计数信息 )
            for (int i = 0; i < array.Length; i++)
            {
                countArray[array[i] - min]++;
            }
            //遍历计数数组 , 生成 累加计数数组 (包含计数信息、位置信息 )
            for (int j = 1; j < k; j++)
            {
                countArray[j] = countArray[j] + countArray[j - 1];
            }

            //从后往前 遍历原序列array ,因为累加计数数组中包含的位置信息
            //是原序列中“同类”元素中最后一个元素的位置信息,比如原序列
            //中包含有三个 1 ,虽然三个 1 大小相等,但是位置不同,根据
            //位置可以将元素区分为1_A, 1_B, 1_C。累加计数数组中是包含位置信息
            //的,但是包含是1_C的位置信息,只有从后往前遍历原序列,然后根据
            //累加计数数组将序列中的元素放入正确位置,才能保证排序算法的稳定性
            //否则,会丧失稳定性,使序列“1_A, 1_B, 1_C”,在排序之后变成:
            //“1_C, 1_B, 1_A” 
            for (int l = array.Length - 1; l >= 0; l--)
            {
                sortedArray[countArray[array[l] - min] - 1] = array[l];
                countArray[array[l] - min]--;
            }

            //sortedArray中的数据是有序的,赋值给 array
            for (int m = 0; m < array.Length; m++)
            {
                array[m] = sortedArray[m];
            }
        }

基数排序:

基数排序与计数排序十分类似,都是“非比较”、“归类”的排序。

需要找出原始序列的最大值,以此确定基数个数。假定最大为1000,则基数数量为 4,四个基数为“千位、百位、十位、个位”。

我们从个位开始,利用“累加计数数组”,对原数组按照个位的大小顺序对原序列进行排序,且需要保证稳定性。然后循环“基数数量”次,分别对剩下的“十位”、“百位”、“千位”进行同逻辑的排序。

从个位开始而不是千位的原因:需要从影响力小的基数开始,否则需要使用递归。

//基数排序
        static void  RadixSort(int[] array) 
        { 
            //生成辅助数组,用于排序
            int[] helpArray = new int[array.Length];

            //找出原序列的最大值,以此确一共有几个基数
            int max = 0;
            for (int i = 0; i < array.Length; i++) {
                if (array[i] > max) {
                    max = array[i];
                }
            }

            //循环确定基数数量,即确定最大值一共有几位数
            int radixNum = 0;
            while (max != 0) {
                max /= 10;   radixNum++;
            }

            //循环基数数量次
            for (int j = 1; j <= radixNum; j++)
            { 
                //生成 计数数组
                int[] countArray = new int[10];
                for (int k = 0; k < array.Length; k++) {
                    int radix = (int)(array[k] % Math.Pow(10, j) / Math.Pow(10, j - 1));
                    countArray[radix]++;
                }

                //计数数组 转换为 累加计数数组
                for (int l = 1; l < countArray.Length; l++) {
                    countArray[l] = countArray[l] + countArray[l - 1];
                }

                //类似计数排序,从后往前遍历原序列
                for (int m = array.Length - 1; m >= 0; m--) {
                    int radix = (int)(array[m] % Math.Pow(10, j) / Math.Pow(10, j - 1));
                    helpArray[countArray[radix] - 1] = array[m];
                    countArray[radix]--;
                }

                //至此,helpArray已经将原数列按基数大小进行了稳定的排序,
                //便将排序好的数组赋值给原序列
                for (int n = 0; n < array.Length; n++) {
                    array[n] = helpArray[n];
                }
            } 
        }

桶排序:

计数排序、基数排序本质都是桶排序。

举报

相关推荐

0 条评论