数据结构_常见排序算法

排序:将一组杂乱无章的数据按照一定的规律(升序或者降序)组织起来

排序码:用来作为排序的依据,是数据元素的一个属性域

排序算法的稳定性:符合排序码的元素相对位置不发生变化

各种常见排序算法的分类:

1.冒泡排序:

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:稳定

算法思路:

(1)按照升序规则,从前往后冒: 

    [0, bound)  是等待排序的区间

    [bound, size)  是已经有序的区间     (bound,cur 从 0 开始,bound < size -1, cur  > size - bound - 1,cur 从前往后,要小于有效区间)

    从第一个出发,碰见比自己大的就交换位置,交换后继续向后走,直到走到最后,这个时候最大的元素就在最后,第二次还是从第一个开始冒,碰见比自己大的就交换,直到比较倒数第二个(因为最后一个已经是最大的了,不需要在比较)……直到每个元素都比较,每次冒出一个未排序区间最大的元素,下一次就减少一次比较。第一重循环就是一共有多少个元素,第二重循坏是一个元素要比较多少次。

// 交换函数
void Swap1(int* a, int* b)
{   
    *a = *a ^ *b;
    *b = *b ^ *a;
    *a = *a ^ *b;
    return;
}

void Swap2(int* a, int* b)
{   
    *a = *a + *b;
    *b = *a - *b;
    *a = *a - *b;
    return;
}

// (以下所有代码使用的交换函数无特别标注,都是Swap3 )
void Swap3(int* a, int* b)
{   
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

// 冒泡排序
// 从前向后冒,降序
//  [bound, size) 代表有序区间
//  [0, bound) 代表等待排序区间
//  cur = 0  从前向后排序
void BubbleSort2(int arr[], int size)
{
    if(arr == NULL || size <= 0){
        return;
    }
    int bound = 0;
    for(; bound < size - 1; bound ++){
        int cur = 0;
        for(; cur < size - bound - 1; cur ++){
            if(arr[cur] < arr[cur + 1]){
                //Swap2(&arr[cur], &arr[cur + 1]);
                Swap3(&arr[cur], &arr[cur + 1]);
            }
        }
    }
    return;
}

(2)按照降序规则,从后往前冒:

    [0, bound)  是已经有序的区间

    [bound, size)  是等待排序的区间 (bound 从0 开始,cur 从 size - 1 开始,bound < size - 1, cur > bound,cur 从后往前,要大于有效区间)

    从最后一个出发,碰见比自己大的就交换位置,交换后继续向前走,直到走到第一个元素,此时第一个元素就是最大的,第二次还是从最后一个开始,直到第二个位置(不需要和第一个比较),这样一直到最后一个,每冒一次,下次就少比较一次。

// 从后往前冒泡,升序
//  [0, bound) 代表有序区间
//  [bound, size) 代表等待排序区间
//  cur = size - 1 从后向前排序
void BubbleSort1(int arr[], int size)
{
    if(arr == NULL || size <= 0){
        return;
    }
    int bound = 0;
    for(; bound < size - 1; bound ++){
        int cur = size - 1;
        for(; cur > bound; cur --){
            if(arr[cur - 1] > arr[cur]){
                Swap1(&arr[cur - 1], &arr[cur]);
            }
        }
    }
    return;
}

2.选择排序:

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:不稳定

算法思路:

    (从前向后,升序)每次从边界出发,第一个元素作为擂主(不动),和第二个元素进行比较,如果擂主小,那么不交换值,cur 继续向后走,下一个值继续和擂主比,直到最后一个元素和擂主比较完,这个时候第二个元素称为擂主,从第三个元素开始比较,直到比到最后。每一次打擂台都有可能打乱原有符合排序规则元素之间的原有位置,所以不稳定。

// 选择排序 升序
// [0, bound)  是有序区间
// [bound, size) 是待排序区间
void SelectSort(int arr[], int size)
{
    if(arr == NULL || size <= 0){
        return;
    }
    int bound = 0;
    for(; bound < size; bound ++){
        int cur = bound + 1;
        for(; cur < size; cur ++){
            if(arr[bound] > arr[cur]){
                Swap(&arr[bound], &arr[cur]);
            }
        }
    }
    return;
}

3.插入排序:

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:稳定

算法思路:

    (升序)     [0, bound)  是已经有序的区间,[bound, size)  是等待排序的区间,bound从 1 开始,cur (cur = bound )来辅助理解完成,前面为一个有序的区间,每次从bound 位置的元素,和有序区间里的元素进行比较,找到一个合适的位置时(边找边搬运,有一个元素比 bound大,就往后搬运一次,直到空出来位置),使用cur来搬运元素,向后搬运,给前面留出一个空位,要提前保存当前bound 的值(搬运会覆盖掉现在的bound),直到搬运到合适的空位置时,cur不再搬运,跳出循环,此时 cur 的位置是空位,用之前保存的 bound值填上空位,直到bound == size 结束大循环,可以理解为所有的元素插入结束。

    特点:(1)如果待排序序列有序性比较高时,比较的次数就少,所以效率就比较高;

              (2)如果待排序列的元素个数较少时,比较的次数也比较少,此时效率也比较高。

// 插入排序  升序
// [0, bound) 为有效区间
// [bound, size)  为待排序区间
void InsertSort(int arr[], int size)
{   
    if(arr == NULL || size <= 1){
        return;
    }
    int bound = 1;
    for(; bound < size; bound ++){
        int bound_value = arr[bound];
        int cur = bound;
        for(; cur > 0; cur --){
            if(bound_value < arr[cur - 1]){
                arr[cur] = arr[cur - 1];
            }else{
                break;
            }
        }
        arr[cur] = bound_value;
    }
    return;
}

4.堆排序:

时间复杂度:O(nlogn)

空间复杂度:O(1)

稳定性:不稳定

算法思路:

    创建堆的方式有两种:

(1)上浮式更新创建:上浮就是让叶子节点向上去找合适的位置,数组的大小为 size ,堆有效结点为 0,从数组的第一个元素开始,插入堆中size++,依照规则(大小堆的父子节点大小规则),当前插入元素和其父节点比较,符合不规则进行交换,child 记录叶子节点的下标,parent记录其父节点的下标,交换后更新 child、parent下标,继续比较,符合的话,继续插入下一个元素(插入到堆的最后一个节点,size++),再与其父节点进行比较,直到对的有效节点个数等于数组的元素个数,堆构建完成。

// 上浮式调整
void AdjustUp(int arr[], int arr_size, int index)
{
    int child = index;
    int parent = (index - 1) / 2;
    while(child < arr_size){
        if(arr[child] > arr[parent]){
            Swap(&arr[child], &arr[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }else{
            break;
        }
    }
    int i = 0;
    for(; i < arr_size; i ++){
        printf("%d ", arr[i]);
    }
    printf("\n");
    return;
}

// 上浮式调整创建堆
void CreateHeapByUp(int arr[], int arr_size)
{
    int index = 0;
    for(; index < arr_size; index ++){
        AdjustUp(arr, arr_size, index);
    }
    return;
}

(2)下沉式更新创建:下沉式调整就是让父节点向下去找合适的位置,假设数组中保存的是一个完全二叉树结构,需要进行调整来完成以一个堆,从最后一个父节点(下标为 ((size-1)-1)/2 )开始,parent记录当前父节点的下标,child 记录其孩子节点中较大的一个子树下标,依照规则比较,交换到合适位置后,再开始下沉前一个父节点,直到堆顶元素也下沉完成后,堆就完成了。

// 下沉式调整
void AdjustDown(int arr[], int heap_size, int index)
{
    // 第一次调整时,父节点是最后一个父节点
    int parent = index;
    int child = 2 * parent + 1;
    while(child < heap_size){
        // 父节点进行下沉时,如果父节点有左右子树,需要比较左右子树的大小,交换较大的子树
        if(child + 1 < heap_size && arr[child + 1] > arr[child]){
            child += 1;
        }
        if(arr[child] > arr[parent]){
            Swap(&arr[child], &arr[parent]);
            parent = child;
            child = 2 * parent + 1;
        }else{
            break;
        }
    }
    return;
}

// 下沉式调整创建堆
void CreateHeapByDown(int arr[], int arr_size)
{   
    // 最后一个元素的下标是 size-1 ,它的父节点下标就是 (arr_size - 1 - 1) / 2
    // 从最后一个父节点开始创建
    int index = (arr_size - 1 - 1) / 2;
    for(; index >= 0; index --){
        // 从最后一个父节点开始下沉,符合规则就进行下沉,然后更新父子节点下边(向上更新),然后继续比较
        // 直到父节点为根节点进行比较后,结束循环
        AdjustDown(arr, arr_size, index);
    }
    return;
}

     堆排序需要借助一个堆来完成,大堆中,父节点的值要大于子节点,小堆中,父节点的值要小于子节点,给定义一个数组,来构建一个堆。

(1)若要进行升序排序,则需要创建一个大堆,对创建好的大堆进行删除堆顶元素,直接删除的话,需要搬运数组元素,可以交换堆顶和最后一个节点,然后 -- 堆的结点个数,这样操作的话,堆的有效元素就不包括最后一个数组元素了,刚好最后一个元素是数组中的最大值。删除后,需要对堆顶元素进行下沉调整,对比左右子树的值大小,堆顶元素与较大的结点值进行交换(借助 child 和 parent 记录下标,第一次下沉操作parent 指向堆顶元素,child指向其孩子结点,交换后继而更新两个下标,直到 堆顶元素移动到符合堆规则位置)(2)若要进行降序排序,需要创建一个小堆,对创建好的小堆也进行删除堆顶元素操作,先交换堆顶元素和最后一个堆结点的位置,再对新的堆顶点进行下沉操作。

// 删除堆顶元素
void DeleteHeapTop(int arr[], int arr_size)
{
    int index = arr_size - 1;
    for(; index > 0; index --){
        Swap(&arr[0], &arr[index]);
        AdjustDown(arr, index, 0);
    }
}

// 堆排序  升序
void HeapSort(int arr[], int arr_size)
{
    if(arr == NULL || arr_size <= 1){
        return;
    }
    CreateHeapByUp(arr, arr_size);
    DeleteHeapTop(arr, arr_size);
    return;
}

5.希尔排序:

时间复杂度:O(n^2)   最好的情况是 O(n^1.3) 

空间复杂度:O(1)

稳定性:不稳定

算法思路:

    希尔排序是进阶版的插入排序,鉴于插入排序的特点,希尔排序采取分组插入排序,先将待排序序列进行分组,以步长( gap ,初始值一般设为 序列元素总个数的 1/2)来区分,0,0+gap,0+2*gap... 为一组 1,1+gap,1+2*gap为一组... 等等,相距 gap 个元素的为一组,对每一组的元素进行直接插入排序,然后减小步长(gap= gap / 2),再进行新的小组内排序,直到步长为 1 ,即对序列进行直接插入排序,这个时候序列已经基本有序,效率比较快。

    每个小组内排序,和插入排序一样,用 bound 来作为小组的边界,cur 和 bound 保持一致来辅助完成排序,bound 开始指向第一个小组的第二个元素,以后向后++,依次再指向第二组、第三组...的第二个元素,走过一个 gap 的步数,就依次到每个小组的第三个元素,以此类推,直到遍历完序列。bound移动一次, cur 要给 bound 元素在当前小组内前面找到一个合适的位置,找到以后,bound++,继续找下一个,用 cur-gap 来标记 cur当前小组的前一个元素。

    希尔排序的效率取决于步长。

// 希尔排序  升序
void ShellSort(int arr[], int size)
{
    if(arr == NULL || size <= 1){
        return;
    }
    // 初始步长定位 size / 2
    int gap = size / 2;
    for(; gap >= 1; gap = gap / 2){
        // gap = 1 时,就等于对数组进行直接插入排序,这时候数组已经基本有序了,效率快
        // gap 每次减少 1/2
        int bound = gap;
        for(; bound < size; bound ++){
            // bound 从第一组的第二个元素开始遍历,直到数组结束,cur辅助来对小组内的元素排序
            int cur = bound;
            int bound_value = arr[bound];
            for(; cur >= gap; cur -= gap){
                // cur 从后往前比较 元素大小,进行交换
                if(arr[cur - gap] > bound_value){
                    arr[cur] = arr[cur - gap];
                }else{
                    break;
                }
            }
            // 填补空位
            arr[cur] = bound_value;
        }
    }
}

6.归并排序:

时间复杂度:O(nlogn)

空间复杂度:O(n)

稳定性:稳定

算法思路:

(1)递归方法:

    利用递归的思想,将带排序序列进行拆分,递归的拆分成更小的区间,直到区间只剩下一个元素时,不需要排序,已经有序了,然后需要将结果归并起来,创建一个临时数组来保存归并的结果,最后将临时数组的值赋给原数组。以下图为例,待排序序列为 {4,3,2,5,6,1},分成橙色划分区间,继续划分成黄色区间,再继续划分成绿色区间,此时绿色区间已经有序,将结果按序归并到一起,然后向上归并黄色区间,再继续向上归并橙色区间,得到的结果就是有序的了。

    下图为划分归并的详细过程:

// 归并数组
void MergeArr(int arr[], int left, int mid, int right, int *tmp)
{
    // 遍历区间,将有效的子区间用tmp数组保存起来
    int left_index = left;
    int right_index = mid;
    int tmp_index = left;
    while(left_index < mid && right_index < right){
        // 左右区间都有元素的时候,进行比较
        if(arr[left_index] < arr[right_index]){
            tmp[tmp_index ++] = arr[left_index ++];
        }
        else{
            tmp[tmp_index ++] = arr[right_index ++];
        }
    }
    // 一个区间的元素遍历完了,直接将另一个区间的剩余元素加到 tmp 数组中
    while(left_index < mid){
        tmp[tmp_index ++] = arr[left_index ++];
    }
    while(right_index < right){
        tmp[tmp_index ++] = arr[right_index ++];
    }
    // 将 tmp 的元素搬运到 原数组中
    int i = left;
    for(; i < right; ++i){
        arr[i] = tmp[i];
    }
    return;
}

// 辅助递归函数
void _MergeSort(int arr[], int left, int right, int* tmp){
    if(right - left <= 1){
        return;
    }
    int mid = left + (right - left) / 2;
    _MergeSort(arr, left, mid, tmp);
    _MergeSort(arr, mid, right, tmp);
    // 每一个小区间进行排序后,修改在数组中的顺序
    MergeArr(arr, left, mid, right, tmp);
    return;
}

// 递归归并排序  升序
void MergeSort(int arr[], int size)
{
    if(arr == NULL || size <= 1){
        return;
    }
    // 用临时数组来保存归并时的有序序列
    int* tmp = (int*)malloc(sizeof(int)*size);
    _MergeSort(arr, 0, size, tmp);
    free(tmp);
    return;
}

(2)非递归方法:

    非递归进行归并排序还是采用分区间的思想,将大区间划分成小区间,然后再进行归并,使用双重循环,第一重循环用来分区间,第二重循环用来进行归并。

    用 gap 来进行分区间,gap 表示区间的元素个数,gap = 1,2,4,8……到数组的元素个数,还是使用三个下标来辅助完成归并,left,right,mid,[left, mid)表示左区间,[mid,right)表示右区间来进行归并,分区间归并后的有序序列存到临时数组 tmp 中方便保存,每次归并从第一个元素开始,用 index 来表示数组下标,index 每循环一次 增加 2*gap,因为一个区间的元素为 gap 个,每次对左右两个区间合并,所以需要跨越两个区间的元素。

    left = index,mid = index + gap, right = index + 2*gap ,注意在移动过程中,mid、right 可能超出数组范围,这样就没法归并最后一个区间,需要将mid、right设为合法值,让其都等于left。

    gap 逐渐增大,当归并的区间是整个数组即 gap = size 时,就完成了排序。下图为详细过程:

// 非递归归并排序  升序
void MergeSortByLoop(int arr[], int size)
{
    if(arr == NULL || size <= 1){
        // 非法输入或者小于两个元素,不需要排序
        return;
    }
    int *tmp = (int*)malloc(sizeof(int)*size);
    // 用步长来分区间的大小
    int gap = 1;
    for(; gap < size; gap = gap*2){
        int index = 0;
        for(; index < size; index = index + 2*gap){
            int left = index;
            int right = index + 2*gap;
            int mid = index + gap;
            if(mid >= size){
                // 防止合并最后一个区间的时候发生越界
                mid = left;
            }
            if(right >= size){
                // 防止合并最后一个区间的时候发生越界
                right = size;
            }
            MergeArr(arr, left, mid, right, tmp);
        }
    }
    free(tmp);
}

7.快速排序:

时间复杂度:O(nlogn)     最坏的情况是 O(N^2)  有两种情况:1.数组完全是反序 2.数组的长度很长

空间复杂度:O(n) 最坏情况 

稳定性:不稳定

算法思路:

1)递归方法:

 (1)交换法:以升序为例,在待排序序列中选取一个基准值(一般选取第一个元素或者最后一个元素),begin 下标从第一个元素向后走,end 下标从最后一个元素向前走,begin 遇到比基准值大的就停下来,end 遇到比基准值小的就停下来。

        ( 基准值是最后一个,begin 先移动;基准值是第一个,end 先移动。)

         交换 begin 和 end 下标所在的值,继续移动 begin 、end ,直到 begin = end ,将 begin 和 end 所在的元素与基准值交换,此时,在基准值之前的元素都小于基准值,之后的元素大于基准值,返回 begin/end 。

   返回的下标作为中间下标,继续分别对左右子区间递归进行以上操作,子区间只有一个元素时,直接返回所在下标,当递归结束的时候,排序就完成了。

// 递归版本1,交换法 
int MethodSwap(int arr[], int left, int right)
{
    if(right - left <= 1){
        return left;
    }
    int standard = arr[right - 1];
    int begin = left;
    int end = right - 1;
    while(begin < end){
        // 以 第一个元素为基准的话,end 先走
        // 以 最后一个元素为基准的话,begin先走
        while(begin < end && arr[begin] <= standard){
            ++begin;
        }
        while(end > begin && arr[end] >= standard){
            --end;
        }
        Swap(&arr[begin], &arr[end]);
    }
    Swap(&arr[end], &arr[right - 1]);
    return end;
}

        (2)挖坑法:以升序为例,在待排序序列中选择一个位置作为“坑”,将坑的值保存起来,begin 下标从头开始,end 下标从尾开始,(选择第一个元素作为坑的,end先开始移动;选择最后一个元素,begin先开始移动)            

                   当 begin 遇到一个比坑值大的元素,就将 begin 位置的元素填到旧坑里(即 end 的位置),这时候 begin 位置就是一个新坑

                   当 end 遇到一个比坑值小的元素,就将 end 位置的元素填到旧坑里(即 begin 的位置),这时候 end 位置就是一个新坑

                  继续移动 begin、end 直到 begin = end 返回 begin/end ,递归的对子区间进行以上操作。当子区间只有一个元素的时候直接返回,递归退出,排序就完成了。

// 递归版本2,挖坑法
int MethodPit(int arr[], int left, int right)
{
    if(right - left <= 1){
        return left;
    }
    // 将坑的元素保存起来,然后进行比较
    int pit = arr[right - 1];
    int begin = left;
    int end = right - 1;
    while(begin < end){
        while(begin < end && arr[begin] <= pit){
            ++begin;
        }
        // 判断是否已经出现新坑
        if(arr[begin] > pit){
            // 需要将新坑的值赋给旧坑,begin 处是新坑
            arr[end] = arr[begin];
        }
        while(end > begin && arr[end] >= pit){
            --end;
        }
        // 判断是否有新坑
        if(arr[end] < pit){
            // 需要将新坑的值保存下来,end 处是新坑
            arr[begin] = arr[end];
        }
    }
    arr[end] = pit;
    return end;
}

// 辅助递归函数
void _QuickSort1(int arr[], int left, int right)
{
    if(arr == NULL || right - left <= 1){
        return;
    }
    // int mid = MethodSwap(arr, left, right);
    int mid = MethodPit(arr, left, right);
    _QuickSort1(arr, left, mid);
    _QuickSort1(arr, mid + 1, right);
    return;
}

// 递归快速排序  升序
void QuickSort1(int arr[], int size)
{
    if(arr == NULL || size <= 1){
        return;
    }
    _QuickSort1(arr, 0, size);
    return;
}

    2)非递归方法:

           处理局部的时候,需要将未处理的部分保存记录,借助栈来完成,栈保存数组的下标位置,先入栈数组的首尾下标。去栈顶元素,如果栈为空的话,说明排序完成;

           取出要处理的区间的首尾坐标,对区间进行交换法或者挖坑法排序,其返回值将区间 再细分成 [left,返回值) ,[返回值,right) 两个区间;

           分别按顺序入栈这两个区间的首尾坐标,继续进行下一次循环,如果区间只有一个元素,直接进入下一次循环;

           栈为空时,序列就是有序的了。

// 循环实现快速排序  升序
void QuickSortByLoop(int arr[], int size)
{
    if(arr == NULL || size <= 1){
        return;
    }
    // 利用栈来完成
    SeqStack s;
    SeqStackInit(&s);

    // 先入栈 0 和 最后一个下标
    SeqStackPush(&s, 0);
    SeqStackPush(&s, size);

    int left = 0;
    int right = 0;

    while(1){
        // 先取出要处理区间的尾坐标
        int ret = SeqStackTop(&s, &right);
        if(ret <= 0){
            // 栈为空说明,已经全部排序完成
            return;
        }
        SeqStackPop(&s);
        // 再取出要处理区间的首坐标
        SeqStackTop(&s, &left);
        SeqStackPop(&s);

        if(right - left <= 1){
            // 如果区间只有一个元素,直接返回
            continue;
        }

        // 找划分区间的中间元素
        int mid = MethodPit(arr, left, right);
        // 先入栈新右区间的首坐标,尾坐标
        SeqStackPush(&s, mid);
        SeqStackPush(&s, right);
        // 再入栈新左区间的首位坐标
        SeqStackPush(&s, left);
        SeqStackPush(&s, mid);
    }
    return;
}

  3)快速排序的改进思想:

        (1)修改选取基值的方法 :从三个元素中选中,开始,中间,最后

        (2)当递归深度一定的时候,不在递归,采取堆排序

        (3)当递归到一定程度时,子区间的元素个数较少,使用插入排序

8.其他排序:

    (1)计数排序:

        利用哈希的思想来进行排序,创建一个数组,将序列中元素出现的次数值在对应的数组下标处,统计相同元素出现的次数,再进行回收到原来的序列,所有元素都遍历完,序列就有序了,只能用于对正整数进行排序。

    (2)基数排序:

    (2)基数排序:

            类似于哈希桶的思想,来进行排序。将序列元素进行分类来完成排序,按照关键字来划分。

    (3)睡眠排序:

            时间复杂度O(0)

            创建与多个线程,每个线程内设定睡眠函数,睡眠函数的值等于序列元素的值,等程序运行结束,序列就有效了。

    (4)猴子排序:

            时间复杂度 O(无穷)

            随机对数组进行排列

猜你喜欢

转载自blog.csdn.net/Cherubim1/article/details/81737530