zoukankan      html  css  js  c++  java
  • 排序算法总结

    1、排序分类
     
    比较排序:冒泡排序、选择排序、插入排序、归并排序、堆排序、快速排序(时间复杂度O(nlogn)~O(n^2))
    非比较排序:计数排序、基数排序、桶排序(时间复杂度O(n))
     
    2、冒泡排序
            
            方法:
    1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
    2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
    3. 针对所有的元素重复以上的步骤,除了最后一个。
    4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(n^2)
    // 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 稳定
    
    // 冒泡排序
    int bubble(int *a, int length){
        int i = 0, j = 0;
        for(i; i < length-1; i++){  // 每次循环都将最大的元素推到最后
            for(j = 0; j < length-1-i; j++)  // 尚未排序的部分
            {
                if (a[j+1]<a[j])  // 交换(不能改为>=,这样会变得不稳定)
                {
                    int tmp = a[j+1];
                    a[j+1] = a[j];
                    a[j] = tmp;
                }
            }
        }
        return 0;
    }
            注:将最大的总是排在最后,两个循环的取值容易出现问题
     
     
    3、冒泡算法改进:鸡尾酒算法(定向冒泡排序)
            
            方法:
            区别于冒泡排序只是从低到高,鸡尾酒排序是先由底到高将最大数送到最后,再由高到低将最小数送到最前面,如此循环
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(n^2)
    // 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 稳定
    
    // 鸡尾酒算法
    int bubble_improve(int * a, int length){
        int left = 0, right = length -1;
        while(left<right){
            for(int i = left; i<right; i++){  // 先正序遍历
                if (a[i]>a[i+1])
                {
                    int tmp = a[i+1];
                    a[i+1] = a[i];
                    a[i] = tmp;
                }
            }
                right--;
            for(int j = right; j>left; j-- ){  // 反序遍历
                if(a[j-1]>a[j]){
                    int tmp = a[j-1];
                    a[j-1] = a[j];
                    a[j] = tmp;
                }
            }
                left++;   
         }
            return 0;
    }
            注: 循环一个从正向一个从反向,一个加一个减,两头都要考虑;两个全局变量的增减是相反的
                  在乱数序列的状态下,鸡尾酒排序与冒泡排序的效率都很差劲。
     
    4、选择排序
            
            有一箱苹果,每次都在所有苹果中选出最大的一个,放在最后,接着又在剩下的苹果中选出最大的一个,放在当前的最后
     
            方法:
            1.初始时在序列中找到最小元素,放到序列的起始位置作为已排序序列;
            2.然后,再从剩余未排序元素中继续寻找最小元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
     
            与冒泡排序的区别:
            冒泡排序是依次交换两个相邻顺序不合法的元素位置;
            选择排序是遍历一次记住最小的元素的位置,最后仅需交换一次就可以放到合适的位置。
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(n^2)
    // 最优时间复杂度 ---- O(n^2)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 不稳定
    
    void Swap(int A[], int i, int j)
    {
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }
    
    // 选择排序(这里是每次选出最小的放在最前端)
    int select(int* a, int length){
        int min = 0;
        for(int i=0; i<length - 1; i++){  //已排
            min =i;
            for(int j = i + 1; j<length; j++){ //未排
                if(a[j]<a[min]){
                    min = j;
                }
            }
            if(i!=min){
                swap(a, i,min);
            }
        }
        return 0;
    }
         
            注:已经排序的序列和尚未排序的序列要区分开,循环条件也要区别开
                     选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。
     
    5、插入排序
            
            类似于抓扑克,手里每抓一个牌,就找到他该在的位置,大于他的牌往后挪出一个空位置,插入即可。
     
            方法:
            对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
    // 分类 ------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
    // 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 稳定
    
    // 插入排序
    int insert(int a[], int n){
        for(int i = 0; i < n; i++){
            int get = a[i];  // 手里抓的牌
            int j = i-1;  // 要和已经抓的牌比大小
            while (j >= 0 && a[j] > get)  // 比抓到的牌大的,都向后挪一个位置
            {
                a[j+1] = a[j];
                j--;
            }
            a[j+1] = get;  // 将抓到的牌放在该放的位置上啦
        }
        return 0;
    }
         注:模拟左右手抓牌的方式,左边有序,右边抓新牌,左手要空出一个合适的位置右手的牌直接插入即可
                   如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。
     
     
    6、插入排序的改进--二分插入排序
     
            方法:
            先跟序列最中间的那个元素比较,如果比最中间的这个元素小,则插入位置在它的左边,否则在它的右边。以当前最中间位置为分割点,如果在左边,则当前最中间位置是待搜索子序列的终点,如果在右边,右边邻接的元素将是待搜索子序列的起点。按照这种原则,继续寻找下一个中间位置,并继续这种过程,直到找到合适的插入位置为止。
     
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(n^2)
    // 最优时间复杂度 ---- O(nlogn)
    // 平均时间复杂度 ---- O(n^2)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 稳定
    
    // 二分插入排序
    void binary_insert(int a[], int n){
       
        int i = 1;
        while(i<n){
            int get = a[i];
            int head =0, tail = i-1;
            while (head<=tail)
            {
                int mid = (head + tail)/2;  // 和中间那个数比较,使用二分法缩短比较的时间
                if(get < a[mid]){
                   tail = mid - 1;
                }else{
                    head = mid + 1;  // 比中间的数字大,就直接从中间后面的序列开始找
                }
            }
            for(int j = i-1; j >= head; j--){
                a[j+1] = a[j];
            }
            a[head] =get;  // 将抓到的牌放在该放的位置上啦
            i++;
        }
    }
            注:每次二分查找都值针对已排序部分,注意循环条件。右手抓的牌要先用一个变量保存,因为之后会被替代掉。
                 所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。
     
     
    7、插入排序高效改进--希尔排序
     
            方法:
             先将整个待排元素序列切割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。
     
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
    // 最优时间复杂度 ---- O(n)
    // 平均时间复杂度 ---- 根据步长序列的不同而不同。
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 不稳定
    
    // 希尔排序(重点:确定步长)
    void shell(int a[], int n){
        int k = 2;
        int step = n/k;  // 步长为一半
        while(step >= 1){
            for(int i = step; i < n; i++){  // 插入排序每次走1,希尔排序每次走step
                int get = a[i];
                int j = i - step;
                while(j>=0 && get<a[j]){
                    a[j+step] = a[j];
                    j -= step;
                }
                a[j+step] = get;
            }
            step /= k;
        }
    }
            注:重点在于步长,与直接插入排序的区别就只是,直接插入每次移动一个,希尔排序是逐步长移动,用step代替逐增的1即可.
                  希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的   元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。
     
     
    8、归并排序
     
            方法:
            归并排序的实现分为递归实现与非递归(迭代)实现
            递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。
            非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,直到归并了整个数组。
    1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
    2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
    3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
    4. 重复步骤3直到某一指针到达序列尾
    5. 将另一序列剩下的所有元素直接复制到合并序列尾
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(nlogn)
    // 最优时间复杂度 ---- O(nlogn)
    // 平均时间复杂度 ---- O(nlogn)
    // 所需辅助空间 ------ O(n)
    // 稳定性 ------------ 稳定
    
    // 归并排序
    void merge(int a[], int left, int mid, int right){
        int length = right - left + 1;
        int *tmp = new int[length];  // 新建辅助数组
        int i = left, j = mid + 1;  // 两边的起始位置
        int position = 0;  // 当前所在位置
        while(i<=mid && j<=right){  // 将两个数组中比较小的放入tmp
            tmp[position++] = a[i]<=a[j] ? a[i++] : a[j++];
        }
        while (i<=mid)  // 此时游标的数字已将全部在tmp中了,只需将左边全部放入
        {
            tmp[position++] = a[i++];
        }
        while(j<=right){
            tmp[position++] = a[j++];
        }
        for(int k = 0; k < length; k++){
            a[left++] = tmp[k];  // 要注意将辅助数组中的内容填入当前数组哦
        }
    }
    // 归并递归(自顶而下) void mergeSortRecursion(int a[], int left, int right){ if(left == right) return ; // 递归结束的条件要加,否则栈溢出 int mid = (left + right)/2; mergeSortRecursion(a, left, mid); mergeSortRecursion(a, mid+1, right); merge(a, left, mid, right); }
    // 归并迭代(自底向上) void mergeSortIteration(int a[], int n){ int left, mid, right; for (int i = 1; i < n; i *= 2) // 先两个两个归并,再四个四个归并 { left = 0; while(left + i < n){ mid = left + i -1; right = mid + i < n ? mid +i : n-1; merge(a, left, mid, right); left = right + 1; } } }
     
            注:最重要的是归并这个函数,new一个数组来存放临时排序结果,再将其赋值给原数组
                递归中一定要注意结束条件,其余实现左右自我调用即可
                迭代就是先两两排序,再四四排序,如此循环,要注意判断之后是否有字数组,将数组索引适当前移即可
     
     
    9、堆排序
     
            方法:
            1、节点下滤:将当前节点和左右孩子比较,若小于左右孩子,则将左右孩子中较大者上滤,自己下滤 到空位,再进行下一次节点下滤
            2、建堆:通过从最后一个有孩子的节点开始遍历,节点下滤 构成最大子堆,节点减1,依次网上遍历,直至构成最大堆
            3、算法:将堆首元素与堆尾元素交换,堆的规模减1,再从堆顶元素开始节点下滤 构成一个新的最大堆,再重复堆首位交换,规模减1,下滤为最大堆的过程。
    // 分类 -------------- 内部比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(nlogn)
    // 最优时间复杂度 ---- O(nlogn)
    // 平均时间复杂度 ---- O(nlogn)
    // 所需辅助空间 ------ O(1)
    // 稳定性 ------------ 不稳定
    
    
    // 归并排序
    void merge(int a[], int left, int mid, int right){
        int length = right - left + 1;
        int *tmp = new int[length];  // 新建辅助数组
        int i = left, j = mid + 1;  // 两边的起始位置
        int position = 0;  // 当前所在位置
        while(i<=mid && j<=right){  // 将两个数组中比较小的放入tmp
            tmp[position++] = a[i]<=a[j] ? a[i++] : a[j++];
        }
        while (i<=mid)  // 此时游标的数字已将全部在tmp中了,只需将左边全部放入
        {
            tmp[position++] = a[i++];
        }
        while(j<=right){
            tmp[position++] = a[j++];
        }
        for(int k = 0; k < length; k++){
            a[left++] = tmp[k];  // 要注意将辅助数组中的内容填入当前数组哦
        }
    }
    
    // 归并递归(自顶而下)
    void mergeSortRecursion(int a[], int left, int right){
        if(left == right)
            return ;  // 递归结束的条件要加,否则栈溢出
        int mid = (left + right)/2;
        mergeSortRecursion(a, left, mid);
        mergeSortRecursion(a, mid+1, right);
        merge(a, left, mid, right);
    }
    
    // 归并迭代(自底向上)
    void mergeSortIteration(int a[], int n){
        int left, mid, right;
        for (int i = 1; i < n; i *= 2)  // 先两个两个归并,再四个四个归并
        {
            left = 0;
            while(left + i < n){
                mid = left + i -1;
                right  = mid + i < n ? mid +i : n-1;
                merge(a, left, mid, right);
                left = right + 1;
            }
        }
    }
     
            注:算法的关键就是节点下滤和递归
                一定要注意size是比最大下标多1,下标从0开始的
                
     
    10、快速排序
     
            
             哨兵i,j都向中间移动,以最左边的数为基准数,因此哨兵j先向左移动,j碰到比基准小的数的话就停止,并将当前的值赋给i所在的地方。此时i开始向右移动,碰到比基准大的值就停止,将他换到j的位置,换成j移动,如此循环直至两个哨兵相遇,将该位置的值设置成标准。至此标准左边的值都比标准小,右边的值都大,这就完成了一轮的快排。
     
            方法:
            1、从序列中挑出一个元素,作为基准,
            2、将比基准小的元素放在基准前面,比基准大的元素放在基准后面。减小分区的规模,递归进行操作,直至分区的大小为0或1。
    // 分类 ------------ 内部比较排序
    // 数据结构 --------- 数组
    // 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
    // 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
    // 平均时间复杂度 ---- O(nlogn)
    // 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
    // 稳定性 ---------- 不稳定
    
    // 快排
    int partition(int a[], int left, int right){
        int standard = a[left];
        while(left < right){
            while((left < right) && standard <= a[right]){  // 以左边为基准,所以右边的哨兵先移动
                right--;
            }
            a[left] = a[right];  // 如果右边的数比标准还小,移到标准左边
            while ((left < right) && standard >= a[left]){  // 左边的哨兵向右移动,碰到比标准小的就向左移一位
                left++; 
            }
            a[right] = a[left];  // 如果左边的数比标准还大,将该数移到标准右边
        }
        a[left] = standard;  // 最后将标准放在中间
        return left;
    }
    void quick_sort(int a[], int left, int right){
        if(left >= right) return;   // 退出条件很重要
        int middle = partition(a, left, right);
        quick_sort(a, 0, middle);  // 递归
        quick_sort(a, middle+1, right);
    }
    注:关于递归,一定要先写退出条件。先画图,理解原理了实现起来就比较简单。
     
    个人博客地址:https://huahua462.github.io/
  • 相关阅读:
    [001]
    SpringBoot默认首页跳转设置
    Tomcat网站根目录设置
    SpringBoot获取前端传递JSON的几种方法
    MySQL调优性能监控之show profile
    MySQL新特性MTS
    Java线程池拒绝策略
    快速排序与荷兰国旗及Partition问题
    数据结构与算法之返回最小和问题
    MySQL之谓词下推
  • 原文地址:https://www.cnblogs.com/huahua12/p/8497208.html
Copyright © 2011-2022 走看看