算法-排序算法

排序算法介绍:

排序的功能是将数据元素的任意序列,重新排列成按照关键字有序的序列

它之所以重要是因为查找操作很重要,而有序的顺序表可以采用效率较高的二分查找(O(logN)),而无序的查找只能顺序查找(O(N)),而创建的二叉搜索树、平衡二叉树、堆的过程其实就是一个排序过程。

排序算法的稳定性:

假设待排序的序列中可能存在两个或两个以上关键字相同的数据,k[i] == k[j]并且i<j,在排序的整个过程中,i必定在j的前面,那么认为该排序算法是稳定的,反之认为是不稳定的排序

1、经典排序:

仅能够完成排序操作,但没有任何的优化动作,没有进行数据比较次数减少的优化,数据交换的次数也没有优化,没有任何优点,不能称得上是算法。

//  经典排序
void classic_sort(TYPE* arr,size_t len)
{
    for(int i=0; i<len-1; i++)
    {   
        for(int j=i+1; j<len; j++)
        {   
            if(arr[j] < arr[i])
            {   
                SWAP(arr[i],arr[j]);
            }   
        }   
    }   
}
2、冒泡排序

排序的过程类似水中气泡上升,越大的气泡上升越快。

从第一个数据开始,让前后相邻的数据两两比较,如果k[i]>k[i+1]则交换它们,每一趟冒泡排序完成一个数据的排序,反复以上过程,直到待排序的数据为1,结束排序

与其他排序相比,冒泡排序对数据的有序性敏感,如果在一趟的排序过程中,没有发生一次交换,则说所有数据前后两两有序,则可以立即停止冒泡排序,提前结束。

注意:如果待排序的序列基本有序,则使用冒泡排序速度最快,因为可以提前结束

//  冒泡排序  
//  最优 O(N) 最差\平均 O(N^2) 稳定的
void bubble_sort(TYPE* arr,size_t len)
{
    //  保证有序性敏感
    bool flag = true;
    for(int i=len-1; i>0 && flag; i--)
    {   
        flag = false;
        for(int j=0; j<i; j++)
        {   
            if(arr[j] > arr[j+1])
            {   
                SWAP(arr[j],arr[j+1]);
                flag = true;
            }   
        }   
    }   
}
3、选择排序

是对经典排序的一种优化,在待排序数据i后面找最小值的下标,如果有比min更小的数据,则先更新min下标,当后面所有数据都比较完后,如果min!=i,则交换min和i的值,继续重复以上步骤。

和经典排序相比,数据的比较次数没有减少,但是数据交换次数大大降低O(N-1),节约了很多的数据交换时间,所以虽然时间复杂度没变化O(N^2),但是它的排序效率比经典排序提高很多

注意:选择排序最突出的特点是数据交换次数最少的,如果待排序的元素的字节数较多,比如:结构、类对象时,则此时使用选择排序速度最快

//  选择排序
//  O(N^2)  不稳定
void select_sort(TYPE* arr,size_t len)
{
    for(int i=0; i<len-1; i++)
    {
        int min = i;
        for(int j=i+1; j<len; j++)
        {   
            if(arr[j] < arr[min]) min = j;
        }   
        if(min != i) SWAP(arr[min],arr[i]);
    }   
}
4、插入排序

往有序的序列中添加新的数据,使得序列保持有序,步骤:假定新的数据位置是i,值是val,让该数据与前面的数据从后往前逐一比较,如果val<arr[i-1],则把前面的数据往后一位拷贝,然后i--,然后重复以上操作,直到val不再小于前面的值,或者i走完所有数据,则该位置就是val应该存放的位置,把数据val放入则完成本次插入操作

使用以上步骤,对无序的序列,看成左右两部分,左边是已经有序的,和右边带插入的无序序列,逐一往左边部分完成插入操作,当无序部分插入完成即完成排序操作。

注意:插入排序适合往已经有序的序列中添加新的数据,优点是排序过程中没有数据交换,节约了大量的无效的时间

//  插入排序
//  时间复杂度  最优O(N) 最差\平均O(N^2)
//  稳定的
void insert_sort(TYPE* arr,size_t len)          
{
    for(int i=1; i<len; i++)
    {   
        //  j是要插入的位置
        int j = i;
        TYPE val = arr[i];  //  待插入数据
        //  与前一个位置相比
        for(j=i; j>0 && arr[j-1] > val; j--)
        {   
            arr[j] = arr[j-1];
        }   
        if(j!=i) arr[j] = val;
    }   
}
5、希尔排序

设计该算法的作者叫希尔,所以叫希尔排序,在插入排序的基础上引入增量的概念(数据在插入的过程中,每次移动的距离),原来的插入排序每次只移动数据一个位置,当数据量比较大,或者数据距离最终正确位置较远时,原来的插入排序速度较慢,在希尔排序中最开始以序列长度的一半作为移动增量,进行第一次插入排序,然后再对半减少移动增量,直到增量为1时,每个数据已经很接近最终位置,最后再完成一次插入排序,就完成排序。

注意:希尔排序适合在数据量大的时候,或者非常无序的序列中添加新的数据。

//  希尔排序 
//  时间复杂度 O(NlogN) 不稳定
void shell_sort(TYPE* arr,size_t len)
{
    //  k是移动增量
    for(int k=len/2; k>0; k/=2)
    {
        for(int i=k; i<len; i++)
        {   
            int val = arr[i],j = i;
            for(j=i; j-k>=0 && arr[j-k] > val; j-=k)
            {   
                arr[j] = arr[j-k];
            }   
            if(j!=i) arr[j] = val;
        }   
    }   
}
6、快速排序

先找到一个标杆位置p,一般是待排序序列的最左边(也可以任意,中间、最右边),然后备份标杆的值val,然后找到待排序序列的最坐标标杆l,和最右标杆r,然后先让r往左走,找比val小的数,找到了把该值赋值给p位置,然后更新p到r;当p和r重合后,换成l从左往右找比val更大数,找到了把该值赋值给p位置,然后更新p到l,重复以上过程,最终l和r相遇时,结束这一次快排,把val的值赋值到相遇点p,结束后,val的左边都比它小,右边都比它大,达到局部有序,然后继续对左右两边分别进行同样快排,最后全部达到有序停止。

注意:快速排序之所以成为快排,综合各种情况下它的表现是最好、速度最快的,所以如果对于待排序的数据不了解时,建议优先选择快排

待排序:67 34 64 43 50 87 08 76
       p                            val:67
       l                    r
第一步:r往左找比val小的
        67 34 64 43 50 87 08 76
        p                           val:67
        l                  r
       把r的8赋值给p 更新p到r
       08 34 64 43 50 87 08 76
                          p     val:67
        l                 r
        l往右找比val大的 把该值付给p 更新p
        08 34 64 43 50 87 87 76
                       p        val:67
                       l  r
        重复以上过程 l 和r会相遇
        08 34 64 43 50 67 87 76
                       p        val:67
                       l  
                       r
//  从arr的left下标到right下标位置进行快排
//  时间复杂度:O(NlogN)
//  不稳定
void _quick_sort(TYPE* arr,size_t len,int left,int right)
{
    if(left >= right) return;
​
    //  记录标杆下标
    int p = left;
    //  备份记录标杆位置的值
    int val = arr[p];
    //  准备左右标杆
    int l = left, r = right;
​
    //  只要l和r还没相遇 一直继续
    while(l < r)
    {
        //  让右标杆 从右往左找 比val小的数值
        while(l < r && arr[r]>=val) r--;
        //  如果找到了更小
        if(l < r)
        {
            //  把更小的值付给标杆
            arr[p] = arr[r];
            //  更新标杆
            p = r;
        }
        //  让左标杆 从左往右找比val大数值
        while(l < r && arr[l] <= val) l++;
        //  如果找到了
        if(l < r)
        {
            arr[p] = arr[l];
            p = l;
        }
    }
​
    //  l和r相遇,该位置就是原来val应该在位置
    arr[p] = val;
    //  对左右两边继续快排
    _quick_sort(arr,len,left,p-1);
    _quick_sort(arr,len,p+1,right);
}
//  快速排序
void quick_sort(TYPE* arr,size_t len)
{
    _quick_sort(arr,len,0,len-1);
}
​
7、堆排序

堆是一种特殊的二叉树,有两种堆,大顶堆(根节点的值大于左右子树,并且所有子树都满足),小顶堆(根节点的值小于左右子树,并且所有子树都满足)

所谓的堆排序就是把一个数据当做大顶堆\小顶堆处理,逐步把堆顶的最值交换到序列的末尾,然后重新调整剩下的数据变回堆结构,重复最终有序,之前写过,不再赘述。

注意:理论上堆排序的速度并不比快排慢,但是对于无序的序列需要先构建成堆结构,时间复杂度已经需要O(N),然后再逐一堆顶出堆完成堆排序O(NlogN),因此无序序列的堆排序不见得很快,所以实际应用中不会使用堆排序,是不稳定的

8、归并排序

先把待排序的序列以k=2为单位进行分组拆分,每组有左右两部分,按照从小到大的顺序合并到另一个内存空间的对应位置,然后让k*=2,继续两组两组合并,最终当k/2>=len则排序完成,在合并的过程,合并的左右部分,都是有序的

归并排序需要一块额外的内存空间,用于存储每次合并的结果,因此节约了大量的数据交换的时间,但是也耗费了额外的空间,也是以空间换时间的策略

注意:如果用户对排序的速度有高要求,但是又不在意内存的消耗,适合使用归并排序

//  时间复杂度:O(NlogN)
//  稳定的
//  把l~p  p+1~r两有序部分 进行合并
//  l左部分最左  p左部分最右
//  p+1 右部分最左 r右部分最右
void __merge(TYPE* arr,size_t len,TYPE* temp,int l,int p,int r)
{
    //  已经都有序了
    if(arr[p] <= arr[p+1]) return;
​
    int i = l, j = p+1,k = l;
​
    //  左右部分还没比完
    while(i<=p && j<=r)
    {
        if(arr[i] <= arr[j])    //  让其稳定
            temp[k++] = arr[i++];
        else
            temp[k++] = arr[j++];
    }
​
    //  把左边剩余的放入后面
    while(i<=p) temp[k++] = arr[i++];
    //  把右边剩余的放入后面
    while(j<=r) temp[k++] = arr[j++];
​
    //  把数据拷贝回arr中对应的位置
    memcpy(arr+l,temp+l,sizeof(TYPE)*(r-l+1));
}
​
//  拆分l~r成两个部分,并形成有序,然后有序合并这两部分
void _merge_sort(TYPE* arr,size_t len,TYPE* temp,int l,int r)
{
    if(l >= r) return;
    int p = (l+r)/2;
    //  拆分成两部分,并各自形成有序
    _merge_sort(arr,len,temp,l,p);
    _merge_sort(arr,len,temp,p+1,r);
    //  合并有序的左右两部分
    __merge(arr,len,temp,l,p,r);
}
​
//  归并排序
void merge_sort(TYPE* arr,size_t len)
{
    //  准备一段同样大小的额外空间
    TYPE* temp = malloc(sizeof(TYPE)*len);
    //  拆分 归并
    _merge_sort(arr,len,temp,0,len-1);
    //  释放额外空间
    free(temp);
}
9、计数排序

找出数据中的最大值和最小值,创建哈希表,把 数据-最小值 当做访问哈希表的下标去给对应位置计数+1,然后遍历所有数据对哈希表进行计数

然后遍历哈希表,当表中的数据大于0时,再通过该位置的下标+最小值得到原数据,并依次放回原数组中,得到有序。

注意:理论上该算法速度非常快,它不是基于比较的算法,在一定范围的正整数数据的排序中,要快于任何一种比较的排序算法,但是有很大的局限性,只适合整形数据,并且数据的差值不宜过大,否则非常浪费内存,因此数据越平均,重复数越多,性价比越高

//  时间复杂度:O(N+k) 
//  稳定的
//  计数排序
void count_sort(TYPE* arr,size_t len)
{
    //  找出最大、最小值
    TYPE min = arr[0],max = arr[0];
    for(int i=1; i<len; i++)
    {   
        if(arr[i] < min) min = arr[i];
        if(arr[i] > max) max = arr[i];
    }   
​
    //  申请哈希表内存
    TYPE* hash = calloc(sizeof(TYPE),max-min+1);
​
    //  标记哈希表
    for(int i=0; i<len; i++)
    {
        hash[arr[i]-min]++;
    }
​
    //  遍历哈希表,还原数据
    for(int i=0,j=0; i<max-min+1; i++)
    {
        while(hash[i]--)
            arr[j++] = i+min;
    }
​
    //  释放哈希表
    free(hash);
}
10、基数排序

先创建10个队列并编号0~9,然后逆序计算出每个数据的个、十、百...的数,然后入队到对应编号的队列中,然后依次把每个队列的值出队,再计算下一位的值并入队,依次循环,直到最大值的所有位数都处理过后,最后一次出队的顺序就是排序成功后的序列

使用该排序也需要额外的内存空间(队列),也是以空间换时间策略

注意:数据的位数不多,差别不大的整型数据才适合

//  基数排序
void radix_sort(TYPE* arr,size_t len)
{
    //  创建10个队列
    ListQueue* queue[10] = {};
    for(int i=0; i<10; i++)
    {
        queue[i] = create_list_queue();
    }
​
    //  循环次数由最大值的位数决定
    TYPE max = arr[0];
    for(int i=1; i<len; i++)
    {
        if(arr[i] > max) max = arr[i];
    }
​
    //  i表示处理哪位 1处理个位,2处理十位,...
    for(int i=1,k=1; max/k >0;k*=10,i++)
    {
        int mod = pow(10,i);
        int div = mod/10;
        //  把每个数据按照规则入到对应的队列中
        for(int j=0; j<len; j++)
        {
            //  获取到每个数的某位
            int index = arr[j]%mod/div;
            push_list_queue(queue[index],arr[j]);
        }
​
        for(int j=0,l=0; j<10; j++)
        {
            //  把队列中的数据依次放回arr中
            while(!empty_list_queue(queue[j]))
            {
                arr[l++] = front_list_queue(queue[j]);
                pop_list_queue(queue[j]);
            }
        }
    }
    //  销毁队列 
    for(int i=0; i<10; i++)
    {
        destroy_list_queue(queue[i]);
    }
}
11、桶排序

桶排序是把待排序的数据,一般根据值的不同范围,划分到不同的“桶”中,然后再根据桶中数据的特点,选择合适的其他的排序算法对各个桶中的数据分别进行排序,最终合并桶中的数据达到了排序的目的

之所以使用桶排序的思想,是因为待排序的数据量非常多的时候,会影响排序算法的性能,桶排序的目的是对数据分类后,降低了数据的规模,并且可能按照特征分类,从而提高排序的效率,和选择更合适的排序算法

//  分桶排序 cnt桶数 range桶中数据的范围    
void _bucket(TYPE* arr,size_t len,int cnt,TYPE range)
{
    //  申请桶内存
    //  bucket指向桶的开头 end指向桶的末尾 数据加入到end位置
    TYPE* bucket[cnt],*end[cnt];
    for(int i=0; i<cnt; i++)
    {
        //  万一全部数据都在一个桶
        bucket[i] = malloc(sizeof(TYPE)*len);
        end[i] = bucket[i];
    }
​
    //  遍历数据,按照范围放入对应桶中
    for(int i=0; i<len; i++)
    {
        for(int j=0; j<cnt; j++)
        {
            if(j*range <= arr[i] && arr[i] < range*(j+1))
            {
                *(end[j]) = arr[i];
                end[j]++;
            }   
        }   
    }
​
    //  对每个桶分别使用其他排序算法来排序
    for(int i=0; i<cnt; i++)
    {
        //  计算出每个桶中元素个数
        int size = end[i] - bucket[i];
        if(size > 1)
            bubble_sort(bucket[i],size);
        //  把桶中的排序后的数据 拷贝回arr对应
        memcpy(arr,bucket[i],sizeof(TYPE)*size);
        arr += size;
        free(bucket[i]);
    }   
}
​
//  桶排序 只对正整数
void bucket_sort(TYPE* arr,size_t len)
{
    //  分4个桶排序
    _bucket(arr,len,4,25);
}

                
  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值