• 经典排序方法及细节小结(2)


    紧接上篇的插入排序  交换排序算法的分析小结,这一篇小结经典排序算法中另外几个算法

    选择排序

    假使对一个n个大小的序列排序

    (1)直接选择排序

    思路:

    ①遍历一遍数组,选择一个最大(小)的值将其放到最后的位置

    ②在剩下的N-1个元素中,再选一个最大(小)的放到后面(倒数第二位置)

    ③不断重复以上操作,当只剩下一个元素时结束算法

    优化

    操作①时 可以找到最大值和最小值,将最大值放到最后的同时还可以把最小值放到最前面,这样的话一趟排序就能确定两个元素的位置了,一定程度上提高了效率。

    优化中注意:如果最大元素本来就是数组首元素即a[left],当最小数和a[left]交换位置后最大元素的位置已经改变了,这就会导致排序错误。

    注意了以上问题就容易写出代码:

    void SelectSort(int* arr, size_t length)
    {
        assert(arr && length > 0);
    
        int left = 0;
        int right = length - 1;
        
        while(left < right)
        {
            int min = left;
            int max = left;
            for(int i = left; i <= right; ++i)
            {
                if(arr[i] < arr[min])
                    min = i;
                if(arr[i] > arr[max])
                    max = i;
            }
            Swap(&arr[left], &arr[min]);
            if(left == max)       //max 对应a[left] 
                max = min;
            Swap(&arr[right], &arr[max]);
            left++; right--;
           // Print(arr, length);
        }
    }

    时间复杂度最好:O(N*N)    最坏:O(N*N)    平均:O(N*N)

    空间复杂度:O(1)

    稳定性:不稳定

    (2)堆排序

    堆排序以其不错排序效率,以及O(1)的空间复杂度成为实际应用中最为广泛的一种排序。

    实现排序之前,先介绍一个建堆和实现堆排都会用到的调整算法:向下调整算法

    它的作用就是将要调整元素向下调整至一个合适的位置,使该元素子树结点都比它小,父辈结点都比它大。

    typedef int DataType;
    
    void AdjustDownToBigHeap(DataType* arr, size_t n, size_t curRoot)
    {
        assert(arr);
    
        size_t parent = curRoot;
        size_t child = (parent<<1) + 1;
        while(child < n)
        {
            if(arr[child] < arr[child+ 1] && child+1 < n)  //找到更大的子结点
                ++child;
            if(arr[child] > arr[parent])        //子结点比父结点大,就交换
                Swap(&arr[child], &arr[parent]);
            //继续往下调整
            parent = child;            
            child = (parent<<1)+ 1;
        }
    }

    堆排实现思路:

    首先将数组按排序码大小建堆,若排升序,建大堆(建大堆很关键);(这里假设排升序;若是排降序,就建小堆)

    begin指向堆顶,end指向堆尾,堆顶堆尾元素相交换 (此时堆顶就是排序码最大的元素了),堆尾元素前移(--end),堆的范围少1。

    再将堆顶元素向下调整,在新范围内让堆继续保持大堆的样式

    重复②③,直到end = 0 结束(n2)>>1

    如对序列{10,20,3,12,16,18,25 ,17,14,19}  建好大堆以后,交换堆头堆尾的值,然后向下调整过程如下:

    注意:

    排升序是不能建小堆的,因为小堆一次只能确定出来最小的那个元素值,虽然小堆中每一个节点的子树的结点都比根结点的值小,但是无法确定左右子结点的谁更小一点,这样排序过程中就无法确定次最小的元素,要排序的话就得重新建堆了,十分拉低效率的。

    堆排代码:

    void HeapSort(DataType* arr, int n)
    {
        assert(arr);
        //建一个大堆
        int i = (n -1)>>1;
        for(; i >= 0; --i)
        {
            AdjustDownToBigHeap(arr, n, i);  //向下调整算法
        }
        //堆头堆尾交换位置,将堆头位置处的值向下调整
        int end = n - 1;
        while(end > 0)
        {
            Swap(&arr[0], &arr[end]);
            AdjustDownToBigHeap(arr, end, 0);
            --end;
        }
    }

    时间复杂度:最好:O(NlogN)    最坏:O(NlogN)  平均:O(NlogN)

    空间复杂度:O(1)

    稳定性不稳定

    归并排序

    归并排序

    归并排序和快排思路基本相同,只不过归并排序额外开辟了O(N)的空间来帮助排序,使得在它最坏的情况也能保持O(NlogN)的时间复杂度,所以相对来说,它是一种更加优良的排序算法,只是耗费了更多的空间。

    思路:

    ①当二分为只剩下两个或一个元素的时候,比较大小排序。

    ②递归回溯时,借助开辟的空间将数组两两进行合并,并且合并后保持有序。

    ③回溯完毕后,整个数组就有序了 。

    注意:

    在利用开辟的空间进行合并,其合并的方法与两条有序链表的合并差不多,只是它不能像链表那样直接进行结点的连接;这里是是按排序码大小依次将放进开辟的数组里面,这样数组里面存放的序列就是有序的了,然后再将这段序列拷贝回原来位置。

    代码:

    void MergeArr(int* arr, int left, int mid, int right)
    {
        int* tmp = (int*)malloc(sizeof(int) * (right -left +1)); //开辟一个中间数组
        int begin1 = left, end1 = mid;
        int begin2 = mid + 1, end2 = right;
        int index = 0;
        
        //归并在一起
        while(begin1 <= end1 && begin2 <= end2)
        {
            if(arr[begin1] <= arr[begin2])
                tmp[index++] = arr[begin1++];
            else
                tmp[index++] = arr[begin2++];
        }
        //如果分成两个数组中一边还有剩余,烤过去
        while(begin1 <= end1)
            tmp[index++] = arr[begin1++];
        while(begin2 <= end2)
            tmp[index++] = arr[begin2++];
        
        //将tmp保存好的有序数据再拷回原数组
        for(int i = left; i<= right; ++i)
            arr[i] = tmp[i-left];
    
        free(tmp);
    }
    void MergeSort(int* arr, int left, int right)
    {
        assert(arr);
        if(left >= right)
            return;
        int mid = left + ((right - left)>> 1);
        MergeSort(arr, left, mid);
        MergeSort(arr, mid+ 1, right);
        MergeArr(arr, left, mid, right);
    }

    时间复杂度:最好:O(NlogN)    最坏:O(NlogN)    平均:O(NlogN)

    空间复杂度:O(N)

    对应两个相同排序码的元素,在排序合并时,在前面的就会先进行合并,合并后并不会影响它们的相对位置

    稳定性:稳定

     计数排序

    计数排序

    基本思路就是对于给定的输入序列中的每一个元素x,统计该序列中值为x的元素的个数 。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。它的原理跟哈希表的K-V模型类似。

    思路:
    ①遍历一遍数组,得出数组的范围range,创建一个大小为range的数组,即哈希表,初始化为全0。
    ②再从头开始遍历数组,数字重复出现一次,在其相应的位置对应的数值加1。
    ③从左到右开始遍历哈希表,将数值不为0的位置的下标存储到原数组中,且数值是多少就存储多少个 。

    注意:

    ①此种排序是依靠一个辅助数组来实现,不基于比较,遍历常数遍数组即可,所以时间复杂度较低;但由于要一个辅助数组C,所以空间复杂度要大一些,由于计算机的内存有限,所以这种算法适合排范围小、值密集的序列,不适合范围很大的序列。

    ②给哈希表分配空间大小时,考虑这样一个问题:例如我有10000个数,范围在10001~20000之间,此时就直接开辟20000的空间大小?很明显这样搞是很浪费的。对此,我们何不先遍历一遍数据,找出最大值与最小值,求出数据范围 (然后用1代表10001,10000代表20000)这样我们就仅需开辟10000个空间即可 节约了大量空间。

        

    时间复杂度:不难看出我们总需要三趟遍历,前两趟统计数据出现次数,遍历原数据、确定辅助数组范围,复杂度为O(N);最后一趟遍历哈希表,向原空间写数据,遍历了range范围次,所以总的时间复杂度为O(N+range)

    空间复杂度:开辟了范围(range)大小的辅助哈希表,所以空间复杂度为O(range)

    稳定性:可稳定

    代码:

    void CountSort(int* arr, int length)  
    {  
        assert(arr && length > 0);  
    
        int max = arr[0];  
        int min = arr[0];  
        //选出最大数与最小数,确定哈希表的大小  
        for (int i = 0; i < length; ++i)  
        {  
            if (arr[i] > max)   
                max = arr[i];  
            if (arr[i] < min)  
                min = arr[i];   
        }  
        int range = max - min + 1;  
    
        int *pCount = (int*)malloc(sizeof(int) * range);  
        memset(pCount, 0, sizeof(int)*range);  //将开辟空间初始化成0  
    
        //确定相同元素的个数
        for (int i = 0; i < length; ++i)  
            pCount[arr[i] - min]++;   
        
        //将数据重新写回数组  
        int j = 0;  
        for (int i = 0; i < range; ++i)  
        {  
            while (pCount[i]-- > 0)  //大小为i+ min的元素有pCount[i]个
                arr[j++] = i + min;  
        }
        free(pCount);
    }  

     

  • 相关阅读:
    ptmalloc内存分配和回收详解(文字版)
    HITCTF2018
    缓冲区溢出保护机制——Windows
    缓冲区溢出保护机制——Linux
    TAMUCTF
    反汇编简介
    apkg命令
    ubuntu基本命令
    ubuntu命令~
    apt-get用法
  • 原文地址:https://www.cnblogs.com/tp-16b/p/8570431.html
Copyright © 2020-2023  润新知