经典排序算法总结

目录

  • 一、常见排序算法时间复杂度
  • 一、冒泡排序
  • 二、选择排序
  • 三、插入排序
  • 四、希尔排序
  • 五、归并排序
  • 六、快速排序
  • 七、堆排序

一、常见排序算法时间复杂度

经典排序算法总结_第1张图片

一、冒泡排序

冒泡排序(Bubble Sort)是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
有一种优化方法是添加一个flag,判断一趟排序是否出现交换,如果没有则说明以及有序退出循环。优化后最好的情况为O(n)

public static void BubbleSort(int[] nums)
        {
            bool isSort = false;
            for (int i = 0; i < nums.Length; i++)
            {
                isSort = false;
                for (int j = 0; j < nums.Length-1-i; j++)
                {
                    if (nums[j] > nums[j + 1])
                    {
                        isSort = true;
                        int temp = nums[j];
                        nums[j] = nums[j + 1];
                        nums[j + 1] = temp;
                    }
                }
                if (isSort)
                {
                	return;
                }
            }

            
        }

二、选择排序

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
对n个数进行排序,每次选出最大的数都要和n-i个数进行比较,所以时间复杂度固定为n^2

public static void SelectSort(int[] nums)
        {
            for (int i = 0; i < nums.Length; i++)
            {
                int indexMax = 0;
                for (int j = 1; j < nums.Length-i; j++)
                {
                    if (nums[indexMax] < nums[j])
                    {
                        indexMax = j;
                    }
                }

                if (indexMax != nums.Length - 1 -i)
                {
                    Swap( ref nums[indexMax], ref nums[nums.Length-1-i]);
                }
            }
        }

三、插入排序

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
每次排序从未排序数列中取出一个数,一共取n次,每次取出都要和已排序数列进行对比,比较n-i次,故时间复杂度固定为O(n^2)

public static void InsertSort(int[] nums)
        {
            for (int i = 1; i < nums.Length; i++)
            {
                
                //取出排序区的最后一个元素索引
                int sortIndex = i - 1;
                //取出未排序区的第一个元素
                int noSortNum = nums[i];
                //在未排序区进行比较
                //移动位置
                //确定插入索引
                //循环停止的条件
                //1.发现排序区中所有元素都已经比较完
                //2.发现排序区中的元素不满足比较条件了
                while (sortIndex >= 0 &&
                       nums[sortIndex] > noSortNum)
                {
                    //只要进了这个while循环 证明满足条件
                    //排序区中的元素 就应该往后退一格
                    nums[sortIndex + 1] = nums[sortIndex];
                    //移动到排序区的前一个位置 准备继续比较
                    --sortIndex;
                }
                //最终插入数字
                //循环中知识在确定位置 和找最终的插入位置
                //最终插入对应位置 应该循环结束后
                nums[sortIndex + 1] = noSortNum;
            }
        }

四、希尔排序

在插入排序基础上改进,希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
为什么希尔排序比插入排序更快?
因为插入一次只能移动一个元素,如果插入一个很小的元素,那么所有已排序元素都要移动一格,希尔排序加入了步长的概念,能快速地把较小的元素移动到前面。
希尔排序的时间复杂度不确定,希取决于它的增量序列。
分析希尔排序的时间复杂度很困难,在特定情况下可以估算排序的比较和元素的移动次数,但要想弄清楚希尔排序的比较次数和元素移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。

public static void ShellSort(int[] nums)
        {
            for (int step = nums.Length / 2; step > 0; step/=2)
            {
                //注意:
                //每次得到步长后 会把该步长下所有序列都进行插入排序
                
                for (int i = step; i < nums.Length; i++)
                {
                    //得出未排序区的元素
                    int noSortNum = nums[i];
                    //得出排序区中最后一个元素索引
                    //int sortIndex = i - 1;
                    //i-step 代表和子序列中 已排序区元素一一比较
                    int sortIndex = i - step;
                    //进入条件
                    //首先排序区中还有可以比较的 >=0
                    //排序区中元素 满足交换条件 升序就是排序区中元素要大于未排序区中元素
                    while (sortIndex >= 0 &&
                           nums[sortIndex] > noSortNum)
                    {
                        //arr[sortIndex + 1] = arr[sortIndex];
                        // 代表移步长个位置 代表子序列中的下一个位置
                        nums[sortIndex + step] = nums[sortIndex];
                        //--sortIndex;
                        //一个步长单位之间的比较
                        sortIndex -= step;
                    }
                    //找到位置过后 真正的插入 值
                    //arr[sortIndex + 1] = noSortNum;
                    //现在是加步长个单位
                    nums[sortIndex + step] = noSortNum;
                }
            }
        }

五、归并排序

归并排序,是创建在归并操作上的一种有效的排序算法。算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
1、分解(Divide):将n个元素分成个含n/2个元素的子序列。
2、解决(Conquer):用合并排序法对两个子序列递归的排序。
3、合并(Combine):合并两个已排序的子序列已得到排序结果。
由于每次分组都能将循环次数减半,所以所有情况下时间复杂度都为O(nlogn)

//第一步:
        //基本排序规则
        //左右元素相比较
        //满足条件放进去
        //一侧用完直接放
        public static int[] Sort(int[] left, int[] right)
        {
            //先准备一个新数组
            int[] array = new int[left.Length + right.Length];
            int leftIndex = 0;//左数组索引
            int rightIndex = 0;//右数组索引

            //最终目的是要填满这个新数组
            //不会出现两侧都放完还在进循环 
            //因为这个新数组的长度 是根据左右两个数组长度计算出来的
            for (int i = 0; i < array.Length; i++)
            {
                //左侧放完了 直接放对面右侧
                if( leftIndex >= left.Length )
                {
                    array[i] = right[rightIndex];
                    //已经放入了一个右侧元素进入新数组
                    //所以 标识应该指向下一个嘛
                    rightIndex++;
                }
                //右侧放完了 直接放对面左侧
                else if( rightIndex >= right.Length )
                {
                    array[i] = left[leftIndex];
                    //已经放入了一个左侧元素进入新数组
                    //所以 标识应该指向下一个嘛
                    leftIndex++;
                }
                else if( left[leftIndex] < right[rightIndex] )
                {
                    array[i] = left[leftIndex];
                    //已经放入了一个左侧元素进入新数组
                    //所以 标识应该指向下一个嘛
                    leftIndex++;
                }
                else
                {
                    array[i] = right[rightIndex];
                    //已经放入了一个右侧元素进入新数组
                    //所以 标识应该指向下一个嘛
                    rightIndex++;
                }
            }

            //得到了新数组 直接返回出去
            return array;
        }

        //第二步:
        //递归平分数组
        //结束条件为长度小于2

        public static int[] Merge(int[] array)
        {
            //递归结束条件
            if (array.Length < 2)
                return array;
            //1.数组分两段  得到一个中间索引
            int mid = array.Length / 2;
            //2.初始化左右数组
            //左数组
            int[] left = new int[mid];
            //右数组
            int[] right = new int[array.Length - mid];
            //左右初始化内容
            for (int i = 0; i < array.Length; i++)
            {
                if (i < mid)
                    left[i] = array[i];
                else
                    right[i - mid] = array[i];
            }
            //3.递归再分再排序
            return Sort(Merge(left), Merge(right));
        }

六、快速排序

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
最坏的情况:序列有序,且将第一个数作为基数,每次分组只能分出一个数来,递归的层数达到最大为n,每次分组需要比较n个数,故最坏情况下为n^2
平均情况下:每次分组都有一半数比基数大,一半数比基数小,递归的层数为logn,每次分组需要比较n个数,故平均情况下为nlogn

//快速排序
        public static void QuickSort(int[] nums,int left, int right)
        {
                
                //递归函数结束条件
                if (left >= right)
                    return;

                
                //记录基准值
                //左游标
                //右游标
                int tempLeft, tempRight, temp;
                temp = nums[left];
                tempLeft = left;
                tempRight = right;
                
                //核心交换逻辑
                //左右游标会不同变化 要不相同时才能继续变化
                while(tempLeft != tempRight)
                {
                    
                    //首先从右边开始 比较 看值有没有资格放到表示的右侧
                    while (tempLeft < tempRight &&
                           nums[tempRight] > temp)
                    {
                        tempRight--;
                    }
                    //移动结束证明可以换位置
                    nums[tempLeft] = nums[tempRight];

                    //上面是移动右侧游标
                    //接着移动完右侧游标 就要来移动左侧游标
                    while (tempLeft < tempRight &&
                           nums[tempLeft] < temp)
                    {
                        tempLeft++;
                    }
                    //移动结束证明可以换位置
                    nums[tempRight] = nums[tempLeft];
                }
                
                //跳出循环后 把基准值放在中间位置
                //此时tempRight和tempLeft一定是相等的
                nums[tempRight] = temp;

                //递归继续
                QuickSort(nums, left, tempRight - 1);
                QuickSort(nums, tempLeft + 1, right);
            
        }

七、堆排序

初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),总时间复杂度为O(nlogn)

//第一步:实现父节点和左右节点比较
        /// 
        /// 
        /// 
        /// 需要排序的数组
        /// 当前作为根节点的索引
        /// 哪些位置没有确定
        static void HeapCompare(int[] array, int nowIndex, int arrayLength)
        {
            //通过传入的索引 得到它对应的左右叶子节点的索引
            //可能算出来的会溢出数组的索引 我们一会再判断
            int left = 2 * nowIndex + 1;
            int right = 2 * nowIndex + 2;
            //用于记录较大数的索引
            int biggerIndex = nowIndex;
            //先比左 再比右
            //不能溢出 
            if( left < arrayLength && array[left] > array[biggerIndex])
            {
                //认为目前最大的是左节点 记录索引
                biggerIndex = left;
            }
            //比较右节点
            if( right < arrayLength && array[right] > array[biggerIndex] )
            {
                biggerIndex = right;
            }
            //如果比较过后 发现最大索引发生变化了 那就以为这要换位置了
            if( biggerIndex != nowIndex )
            {
                int temp = array[nowIndex];
                array[nowIndex] = array[biggerIndex];
                array[biggerIndex] = temp;

                //通过递归 看是否影响了叶子节点他们的三角关系
                HeapCompare(array, biggerIndex, arrayLength);
            }
        }

        //第二步:构建大堆顶
        static void BuildBigHeap(int[] array)
        {
            //从最大的非叶子节点索引 开始 不停的往前 去构建大堆顶
            for (int i = array.Length / 2 - 1; i >= 0 ; i--)
            {
                HeapCompare(array, i, array.Length);
            }
        }

        //第三步:结合大堆顶和节点比较 实现堆排序 把堆顶不停往后移动
        static void HeapSort(int[] array)
        {
            //构建大堆顶
            BuildBigHeap(array);
            //执行过后
            //最大的数肯定就在最上层

            //往屁股后面放 得到 屁股后面最后一个索引
            for (int i = array.Length - 1; i > 0; i--)
            {
                //直接把 堆顶端的数 放到最后一个位置即可
                int temp = array[0];
                array[0] = array[i];
                array[i] = temp;

                //重新进行大堆顶调整
                HeapCompare(array, 0, i);
            }
        }

你可能感兴趣的:(排序算法,算法,数据结构)