[数据结构] 八大排序,快进来学习了


以下的排序方式,都将按照升序方式进行排列。
用例为:
   int arr[] = { 2, 1, 4, 6, 7, 0, 5, 9, 3, 8 };

基本概念:

稳定性:针对记录中相同的数据,在排序前后的顺序是否一致。
内部排序:所有数据一次加载到内存中
外部排序:不需要将所有数据加载到内存中

一. 插入排序

  基本思想:将待排序列的记录按照其元素的大小逐个插入到一个已经排好序的有序序列中,直到所有记录插入完成,得到一个新的序列。

1. 直接插入排序

实现原理

  1. 找到待插入元素在集合中的位置
      对于一个无序数组,从前向后进行排序,每一个元素都在其前方寻找插入位置,因为前方数据已经通过插入排序变成了有序序列。
  2. 插入元素

图解
在这里插入图片描述
时间空间复杂度

  时间复杂度:最坏情况下,序列为降序需要排升序复杂度为 O(n^2),最好情况下序列基本有序 O(n)
  空间复杂度:没有借助辅助空间,O(1)

稳定性 稳定

应用场景

  根据时间复杂度可知,该排序适用于数据较少,或序列基本有序的情况下。

代码实现

// 插入排序
// 对于一个无序序列,选定一个元素,从前开始向后寻找插入位置,同时移动数组内元素
void InsertSort(int* arr, int n)
{
    int i;
    for (i = 1; i < n; i++)
    {
        int key = arr[i];
        int end = i - 1;

        // 寻找插入位置,并且将元素向后进行搬移
        while (end >= 0 && arr[end] > key)
        {
            arr[end + 1] = arr[end];
            end--;
        }

        // 插入元素
        arr[end + 1] = key;
    }
}

2. 希尔排序

实现原理

  如果数据量过大,则直接插入排序就不能够使用了。但是还想利用插入排序的思想进行排序,则就可以使用希尔排序。

具体实现思想

希尔排序又称缩小增量法。

  1. 给定分组大小,例如给定分组值 gap=3
    gap值我们一般给定 gap = gap / 3 + 1
  2. 根据 gap 作为步长,每次加上gap值进行跳转,对组内的数据进行插入排序,使组内元素先有序,这样在最后排序时,组内元素就相对有序,搬移的元素次数也就少了很多
  3. 不断缩小gap,当gap=1时,序列有序。

图解
在这里插入图片描述

时间空间复杂度

时间复杂度:与gap取值有关,所以无法准确计算。
空间复杂度:没有借助辅助空间 O(1)

稳定性 不稳定

代码实现

// 希尔排序
// 使用插入排序的思想,进行排序
// 插入排序适合数据量小并且数据接近有序的情况,并不适合数据量巨大,并且杂乱的情况
// 思想:
//    将数据进行分组处理,对每一组数据分别进行插入排序,开始时先指定分组间隔,后续不断缩小间隔进行分组排序
void ShellSort(int* arr, int n)
{
    int gap = n;
    int i;

    // 按照不同间隔进行插入排序
    while (gap > 1)
    {
        gap = gap / 3 + 1;

        // 按照gap对元素进行分组 
        for (i = gap; i < n; i++)
        {
            int end = i - gap;
            int key = arr[i];

            // 查找插入位置
            while (end >= 0 && arr[end] > key)
            {
                arr[end + gap] = arr[end];
                end -= gap;
            }

            arr[end + gap] = key;
        }
    }
}

二. 选择排序

  基本思想:每一次从待排序的序列中选出最大或者最小的元素,放在序列开始或者末尾的位置,直至所有元素排序完成。

1. 直接选择排序

实现原理

  在待排序列中寻找最大值,找到之后放在待排序列末尾,不断缩小待排序列,直到序列中只剩余一个元素。
  在寻找最大值的同时也可以同时寻找最小值,将最大最小值放在序列头尾,可以提高此算法效率。

图解
在这里插入图片描述

时间空间复杂度

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

稳定性 不稳定

代码实现

// 在序列中寻找最大值,找到之后放在序列末尾,逐渐缩小该序列
void SelectSort(int* arr, int n)
{
    int i, j;
    for (i = 0; i < n - 1; i++)
    {
        int maxPos = 0;

        // 寻找最大值, 记录位置i
        // 每次寻找完后,都会与序列最后的元素进行交换,所以寻找最大值时,不需要寻找已经在队列最后的元素
        for (j = 1; j < n - i; j++)
        {
            if (arr[maxPos] < arr[j])
            {
                maxPos = j;
            }
        }

        // 将所找最大值放在序列末尾
        if (arr[maxPos] != arr[n - i - 1])
        {
            Swap(&arr[maxPos], &arr[n - i - 1]);
        }
    }
}

// 选择排序的升级:在每次寻找最大值的同时,寻找序列中的最小值
// 最大值放在序列末尾,最小值放在序列头,每次循环后缩小查找范围
void SelectSortOP(int* arr, int n)
{
    int end = n - 1;
    int begin = 0;

    while (begin < end)
    {
        int maxPos = begin;
        int minPos = begin;
        int index = begin + 1;

        while (index < end)
        {
            if (arr[maxPos] < arr[index])
            {
                maxPos = index;
            }

            if (arr[minPos] > arr[index])
            {
                minPos = index;
            }

            index++;
        }

        if (maxPos != end)
        {
            Swap(&arr[maxPos], &arr[end]);
        }

        // 如果最小值的位置在end 上述交换完成后,会影响接下来的交换
        if (minPos == end)
        {
            minPos = maxPos;
        }

        if (minPos != begin)
        {
            Swap(&arr[minPos], &arr[begin]);
        }

        end--;
        begin++;
    }
}

2. 堆排序

  对二叉树堆存储方式不了解的话可以点击此链接进行查看 树与二叉树

实现原理

根据二叉树堆的性质进行排序

  1. 建堆 排升序建大堆,排降序建小堆。建堆时利用向下调整算法进行调整,从倒数第一个非叶子结点的位置开始一直调整到根结点。
  2. 利用堆删除的思想进行排序。

图解
此用例为:int arr[] = {1, 2, 5, 3, 7, 8}
在这里插入图片描述

时间空间复杂度

时间复杂度:O(log2 n)
空间复杂度:O(1)

稳定性 稳定

代码实现

void AdjustDown(int* arr, int n, int root)
{
    int child = root * 2 + 1;

    while (child < n)
    {
        // 先寻找根节点的两个孩子那个比较小
        if (child + 1 < n && arr[child] < arr[child + 1])
        {
            child += 1;
        }

        // 如果根节点小于孩子 则进行交换
        if (arr[root] < arr[child])
        {
            Swap(&arr[root], &arr[child]);

            // 交换后给结点可能还是不满足堆特性,继续对该结点进行交换
            root = child;
            child = root * 2 + 1;
        }
        else
        {
            return;
        }
    }
}

// 堆排序
// 先利用向下调整算法,调整堆为大堆或小堆,之后按照删除堆元素的思想进行排序
// 默认排升序 建大堆
void HeapSort(int* arr, int n)
{
    // 建堆
    int i;
    for (i = (n - 2) / 2; i >= 0; i--)
    {
        AdjustDown(arr, n, i);
    }

    // 利用堆删除思想进行排序
    int end = n - 1;
    while (end > 0)
    {
        Swap(&arr[0], &arr[end]);
        AdjustDown(arr, end, 0);
        end--;
    }
}

三. 交换排序

  基本思想:根据序列中两个元素值得比较结果来决定是否交换两个元素在序列中的位置,交换排序的特点:将键值较大的记录向序列尾部移动,键值较小的元素向前移动。

1. 冒泡排序

实现原理

逐个比较相邻两元素值,如果满足则交换两值的位置,直至每个元素比较完成。 每次排序都可以确定下来元素最大的位置。

图解
在这里插入图片描述
时间空间复杂度

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

稳定性 稳定

代码实现

void BubbleSort(int* arr, int n)
{
    int i, j;

    for (i = 0; i < n - 1; i++)
    {   
        int flag = 0;

        for (j = 0; j < n - i - 1; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                flag = 1;
                Swap(&arr[j], &arr[j + 1]);
            }
        }

        if (0 == flag)
        {
            break;
        }
    }
}

2. 快速排序

  快排思想:在数据序列中,随便找一个基准值,按照该基准值划分为两个部分,小于基准值的放在左部,大于基准值的放在右部。先对左部递归上述操作,在对右部递归完成上述操作。递归完成后,就变成了有序序列。
  在递归时每次都需要将序列不断划分为两个部分,所以按照何种方式进行划分序列, 并且如何让左右序列有序,就使用到了以下三种方法。

快排递归代码

// 在数据序列中,随便找一个基准值,将小于基准的放在一侧,大于基准值的放在另一侧
void QuickSortHelper(int* arr, int left, int right)
{
    // 如果只剩余一个元素则不需要排序
    if (right - left <= 1)
    {
        return;
    }

    int div = PartSort3(arr, left, right);

    QuickSortHelper(arr, left, div);
    QuickSortHelper(arr, div + 1, right);
}

2.1 hoare法

实现原理

  采用双指针的防止,对区间左右各放置一个指针,左侧指针查找大于基准值的元素,右侧指针查找小于基准值的元素,当两指针都找到对应元素之后,交换两指针的元素。直到两指针相遇,停止交换。
  在上述操作开始前,将基准值放在序列末尾,在两指针相遇后,再将基准值与相遇位置进行交换

图解
在这里插入图片描述

时间空间复杂度

时间复杂度:O(n) 相当于遍历了一遍数组

代码实现

// 单个分割的方法时间复杂度:O(N)
int Partion1(int array[], int left, int right)
{
	int begin = left;
	int end = right - 1;
	int mid = GetMiddleIndex(array, left, right);
	Swap(&array[mid], &array[right - 1]);
	int key = array[right-1];

	while (begin < end)
	{
		// 让begin从前往后找,找比基准值大的元素
		while (begin < end && key >= array[begin])
			begin++;

		// 让end从后往前找,找比基准值小的元素
		while (begin < end && key <= array[end])
			end--;

		if (begin < end)
		{
			Swap(&array[begin], &array[end]);
		}
	}
	
	if (begin != right-1)
	{
		Swap(&array[begin], &array[right - 1]);
	}

	return begin;
}

2.2 挖坑法

实现原理

  与hoare方法基本相同,同样采用双指针的方式,左指针寻找比基准值大的元素,右指针寻找比基准值小的元素。与上述不同的是,在找到比基准值大或小的值之后,不等待继续查找使两个指针同时满足条件,而是立即将end指针或begin指针值填入该位置。

图解
在这里插入图片描述

代码实现

// 挖坑法
// 与上述结构相似,分别从左右寻找合适的值,找到之后不进行两指针的交换,而是将begin的值放在end位置,相互补充
int PartSort2(int* arr, int left, int right)
{
    int begin = left;
    int end = right;
    int midPos = GetMidIndex(arr, left, right);

    Swap(&arr[midPos], &arr[right]);
    int key = arr[right];

    while (begin < end)
    {
        // 寻找挖坑位置
        while (begin < end && arr[begin] <= key)
        {
            begin++;
        }

        // begin变成了坑
        if (begin < end)
        {
            arr[end] = arr[begin];
        }

        while (begin < end && arr[end] >= key)
        {
            end--;
        }

        // 填上begin的坑, end变成了坑
        if (begin < end)
        {
            arr[begin] = arr[end];
        }
    }

    // 填上最后的坑
    arr[begin] = key;

    return begin;
}

2.3 前后指针法

图解
在这里插入图片描述

代码实现

// 前后指针法
// 画图理解
int PartSort3(int* arr, int left, int right)
{
    int cur = left;
    int prev = cur - 1;

    int midPos = GetMidIndex(arr, left, right);
    Swap(&arr[midPos], &arr[right]);

    int key = arr[right];

    while (cur < right)
    {
        if (arr[cur] < key && ++prev != cur)
        {
            Swap(&arr[cur], &arr[prev]);
        }

        cur++;
    }

    Swap(&arr[prev + 1], &arr[right]);

    return prev;
}

3. 快排的非递归实现

实现原理

  递归转为非递归,一般使用的都是栈的方式,通过将待排区间进行入栈出栈,左右区间交叉排序。由于这种方法较好理解,就不再画图解释。

代码实现

// 利用栈的性质将递归转换为循环方式
// 将区间下标进行入栈,先入左边下标,再入右边下标
void QuickSortNonRHelper(int* arr, int left, int right)
{
    Stack s;
    StackInit(&s);

    StackPush(&s, left);
    StackPush(&s, right);

    while (!StackEmpty(&s))
    {
        right = StackTop(&s);
        StackPop(&s);
        left = StackTop(&s);
        StackPop(&s);

        if (right - left > 1)
        {
            int div = PartSort2(arr, left, right);

            // 计算左区间内容 [left, div)
            StackPush(&s, left);
            StackPush(&s, div);

            // 计算右区间 [div + 1, right]
            StackPush(&s, div + 1);
            StackPush(&s, right);
        }
    }

    StackDestroy(&s);
}

4. 影响快排效率的因素

  如果该序列有序,并且每次取值都取的使序列中最大值作为基准值,则递归时的二叉树结构会退化为单只树,性能会变得极差。时间复杂度变为O(n^2)。但是可以优化它的取基准值的方式,来进行优化。我们采用的方式是三值取中法(取出序列中 头 中 尾 三个元素,取值在中间的那个作为基准)。优化后的平均时间复杂度为O(n*log n)

四. 归并排序

1. 递归实现

实现原理

  归并排序是建立在归并操作的一种有效的排序算法,采用分治思想。
  基本过程是先分再归,将数组不断平均划分,直到只剩下一个元素,再根据合并两个有效数组的方式对分开的序列进行合并。直到整个数组合并完成。
先递归再归并。

图解
在这里插入图片描述

时间空间复杂度

时间复杂度:O(n* log n) 每一层都是O(n) 递归深度为O(log n)
空间复杂度:O(n)

稳定性 稳定

代码实现

// 思路与合并两个有序数组相同
void MergeArr(int* arr, int left, int mid, int right, int* res)
{
    int begin1 = left;
    int end1 = mid;
    int begin2 = mid;
    int end2 = right;

    int index = left;

    // 进行归并排序,谁小就把谁放到res数组里
    while (begin1 < end1 && begin2 < end2)
    {
        if (arr[begin1] < arr[begin2])
        {
            res[index++] = arr[begin1++];
        }
        else
        {
            res[index++] = arr[begin2++];
        }
    }

    // 把没有归并完的元素放到res数组里
    while (begin1 < end1)
    {
        res[index++] = arr[begin1++];
    }

    while (begin2 < end2)
    {
        res[index++] = arr[begin2++];
    }
}

void MergeSortHelper(int* arr, int left, int right, int* res)
{
    if (right - left <= 1)
    {
        return;
    }

    int mid = left + (right - left) / 2;

    // 递归拆分左半部
    MergeSortHelper(arr, left, mid, res);
    // 递归拆分右半部
    MergeSortHelper(arr, mid, right, res);

    // 对拆分数据进行归并排序
    MergeArr(arr, left, mid, right, res);

    // 将数据放回至原数组
    memcpy(arr + left, res + left, sizeof(int) * (right - left));
}

// 思路与合并两个有序数组相类似
// 找到数组的中间位置 分别对左部和右部进行拆分,一直重复上述操作,直到剩余一个元素,对数据进行归并。
void MergeSort(int* arr, int n)
{
    int* res = (int*)malloc(sizeof(int) * n);
    if (NULL == res)
    {
        return;
    }

    MergeSortHelper(arr, 0, n, res);

    free(res);
}

2. 非递归实现

实现原理

省区了拆分过程,直接开始归并,可以直接将序列看成是 n 个分组,不断扩大gap值,每次扩大2倍,直到归并完成。

代码实现

// 归并排序的迭代实现
// 对序列直接进行归并,省去了分组的过程,以gap值作为归并过程,gap从1开始,2倍扩大
void MergeSortNonR(int* arr, int n)
{
    int gap = 1;

    int* res = (int*)malloc(sizeof(int) * n + 1);

    int i;
    int left, mid, right;
    while (gap < n)
    {
        for (i = 0; i < n; i += 2 * gap)
        {
            left = i;
            mid = i + gap;
            right = mid + gap;

            if (mid > n)
            {
                mid = n;
            }

            if (right > n)
            {
                right = n;
            }

            MergeArr(arr, left, mid, right, res);
        }

        gap *= 2;
        // 将数据放回至原数组
        memcpy(arr, res, sizeof(int) * n);
    }

    free(res);
}

五. 计数排序

实现原理

适用于有固定区间的排序。

  1. 统计区间中每个元素出现的次数
  2. 根据计数结果回收数据,按照计数数组的下标对数据进行回收

注意事项:先要确定数据集中的范围,根据范围进行计数数组的开辟

时间空间复杂度

时间复杂度:O(n)
空间复杂度:O(m) m为序列范围

稳定性 稳定

代码实现

// 计数排序
// 给定的时一个数据范围,记录该范围内每个数据出现的次数,根据计数结果回收数据
void CountSort(int* arr, int n)
{
    // 
    int maxVal = arr[0];
    int minVal = arr[0];

    int i, j;
    for (i = 0; i < n; i++)
    {
        if (arr[i] > maxVal)
        {
            maxVal = arr[i];
        }
        if (arr[i] < minVal)
        {
            minVal = arr[i];
        }
    }

    int size = maxVal - minVal;

    int* countArr = (int*)malloc(sizeof(int) * size + 1);
    if (NULL == countArr)
    {
        return;
    }

    // 记录每个数字出现的次数
    for (i = 0; i < n; i++)
    {
        countArr[arr[i] - minVal]++;
    }

    int index = 0;
    // 根据字符出现的次数对元素进行回收
    for (i = 0; i < n; i++)
    {
        while (countArr[i] > 0)
        {
            arr[index++] = i + minVal;
            countArr[i]--;
        }
    }

    free(countArr);
}

总结

以上就是今天排序的所有内容了,对于排序这部分,我建议一定要多练习,并且一定要理解实现的原理,对算法稍有不理解一定要画图,根据图去理解一定是最好的理解方式。 冲冲冲!!!

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值