自学内容网 自学内容网

数据结构——排序(1)

数据结构——排序(1)

一、排序

1.概念:

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

2.运用:

购物筛选排序,院校排名等。

3.常见排序算法

在这里插入图片描述

二、实现排序算法

1.插入排序

1.1直接插入排序

基本思想

待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

实际中我们玩扑克牌时就用了插入排序的思想。

动图理解:

在这里插入图片描述

当插⼊第 i(i>=1) 个元素时,前⾯的 array[0],array[1],…,array[i-1] 已经排好序,此时用 array[i] 的排序码与 array[i-1],array[i-2],… 的排序码顺序进⾏⽐较,找到插⼊位置即将 array[i] 插⼊,原来位置上的元素顺序后移。

代码实现:

void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;//有序区间的最后一个数据
int tmp = arr[end + 1];//有序区间的后一个数据

while (end >= 0)
{
if (arr[end] > tmp)//当arr[end]>tmp,两值交换
{
arr[end + 1] = arr[end];//把tmp放在end的位置
end--;  //end移动到有序区间的倒数第二个数据
}
else     //当arr[end]<=tmp,不做处理,结束循环
{
break;
}
}
arr[end + 1] = tmp;
}
}
  • 为什么for循环的判断条件是i<n-1,而不是i<n呢?

因为当end=n-1时,tmp=arr[n],这里越界了,所以for循环的判断条件是i<n-1。

  • 时间复杂度

最差的情况(数组有序且为降序的情况下时间复杂度最差):时间复杂度为O(N2)

最好的情况(数组有序且为升序的情况下时间复杂度最差):时间复杂度为O(N)。

  • 空间复杂度:O(1)
1.2希尔排序

曾经我们学过冒泡排序,我们知道冒泡排序的算法效率极低(最差的时间复杂度为O(N2)),所以在实际工作中,我们不会用到它。即冒泡排序只有教学意义,没有实际意义。

直接插入排序有实际意义吗?

它有实际意义,但是要优化一下。当数组为降序序列时,直接插入排序还能得到优化吗?这里我们就要引出——希尔排序

希尔排序法⼜称缩小增量法,是一种改进的插入排序算法。它通过比较相距一定间隔的元素来进行排序,逐步缩小间隔,最终间隔为1时,算法退化为普通的插入排序。希尔排序的名称来源于其发明者Donald Shell。

基本思想:

希尔排序法的基本思想是:先选定⼀个整数(通常是gap = n/3+1),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。

以排序数组为例(gap取2)

在这里插入图片描述

代码实现:

void ShellSort(int* arr, int n)
{
int gap = n;//6//gap表示我们要分多少组
while (gap > 1)
{
gap = gap / 3 + 1;//6除3=2,2除以3=0 //+1是为了保证最后一次gap一定为1
for (int i = 0; i < n - gap; i ++)
{
int end = i;
int tmp = arr[end + gap];//tmp最后一个取值要保证不越界
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}

时间复杂度分析:

外层循环:

外层循环的时间复杂度可以直接给出为: O(log2 n) 或者 O(log3 n) ,即 O(log n)

内层循环:在这里插入图片描述

假设⼀共有n个数据,合计gap组,则每组为n/gap(大致)个;在每组中,插⼊移动的次数最坏的情况下为 S=1 + 2 + 3 + ……+ (n/gap-1),⼀共是gap组,因此:

总计最坏情况下移动总数为:gap ∗ S

gap取值有(以除3为例):n/3 n/9 n/27 … 2 1

一 一带入:

  • 当gap为n/3时,移动总数为: n

  • 当gap为n/9时,移动总数为: 4n

  • 最后⼀趟,数组已经已基本有序了,gap=1即直接插入排序,移动次数就是n

通过以上的分析,可以画出这样的曲线图:

在这里插入图片描述

因此,希尔排序在最初和最后的排序的次数都为n,即前⼀阶段排序次数是逐渐上升的状态,当到达某⼀顶点时,排序次数逐渐下降⾄n,⽽该顶点的计算暂时⽆法给出具体的计算过程。

希尔排序时间复杂度不好计算,因为 gap 的取值很多,导致很难去计算,因此很多书中给出的希尔排序的时间复杂度都不固定。《数据结构(C语⾔版)》— 严蔚敏书中给出的时间复杂度为:在这里插入图片描述

总而言之,希尔排序的时间复杂度范围为:O(N1.3)~O(N2)

  • 希尔排序的时间性能优于直接插入排序的原因:

在希尔排序中,随着增量的减小,元素的移动次数会显著降低。当数据接近有序时,直接插入排序的性能显著提升,而希尔排序则更早地创建了部分有序的数据集合,使得后面的排序过程更加高效。

2.选择排序

2.1直接选择排序

基本思想:

每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

动图理解:

在这里插入图片描述

  1. 在元素集合 array[i]–array[n-1] 中选择关键码最⼤(⼩)的数据元素

  2. 若它不是这组元素中的最后⼀个(第⼀个)元素,则将它与这组元素中的最后⼀个(第⼀个)元素交换

  3. 在剩余的 array[i]–array[n-2](array[i+1]–array[n-1]) 集合中,重复上述步骤,直到集合剩余 1 个元素

代码实现:

void SelectSort(int* arr,int n)
{
    int begin=0;
    int end=n-1;
    
    while(begin<end)
    {
        int mini=begin,maxi=begin;
        for(int i=begin+1;i<=end;i++)
        {
            if(arr[i]>arr[maxi])
            {
                maxi=i;
            }
            if(arr[i]<arr[mini])
            {
                mini=i;
            }
        }
       //避免maxi begin都在同一个位置,begin和mini交换之后,mini数据变成了最小的数据
        if(maxi==begin)
        {
            maxi=mini;
        }
        Swap(&arr[mini],&arr[begin]);
        Swap(&arr[maxi],&arr[end]);
        
        ++begin;
        --end;
    }
}
  • 时间复杂度是O(n2).
2.2堆排序

堆排序是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀种。它是通过堆来进行选择数据。需要注意的是排升序要建⼤堆,排降序建小堆。在上篇二叉树(下)中我们已经实现过堆排序,这⾥不再细述。

3.交换排序

3.1冒泡排序

冒泡排序是⼀种最基础的交换排序。之所以叫做冒泡排序,因为每⼀个元素都可以像小气泡⼀样,根据自身大小⼀点⼀点向数组的⼀侧移动。

动图理解:

在这里插入图片描述

代码实现:

void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 0; j < n - i - 1; j++)
{
//升序
if (arr[j] > arr[j + 1])
{
exchange = 1;
Swap(&arr[j], &arr[j + 1]);
}
}
if (exchange == 0)
{
break;
}
}
}
  • 时间复杂度(最差):O(N2).
3.2快速排序(采用二叉树递归的思想)

快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法。

基本思想:

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左⼦序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
//[left,right]--->找基准值mid
int keyi = _QuickSort(arr, left, right);
//左子序列:[left,ley-1]
QuickSort(arr, left, keyi - 1);
//右子序列:[keyi+1,right]
QuickSort(arr, keyi + 1, right);
}
  • 空间复杂度:O(logn)
  • 时间复杂度:O(nlogn)

找基准值的三种方法

  • hoare版本

算法思路 :

1)创建左右指针,确定基准值;

2)从右向左找出⽐基准值小的数据,从左向右找出比基准值大的数据,左右指针数据交换,进⼊下次循环.

问题1:为什么跳出循环后right位置的值⼀定不⼤于key?

当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于key,因此right此时指向的数据⼀定不⼤于key

问题2:为什么left 和 right指定的数据和key值相等时也要交换?

相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据⼤量重复时,无法进⾏有效的分割排序。

int _QuickSort1(int* arr, int left, int right)
{
int keyi = left;
++left;

while (left<=right)//left和right相遇的位置的值比基准值要大
{
while (left <= right && arr[right] > arr[keyi])
{
right--;
}
//right找到比基准值小或等于
while (left <= right && arr[left] < arr[keyi])
{
left++;
}
if (left <= right)
{
Swap(&arr[left++], &arr[right--]);
} 
}

Swap(&arr[keyi], &arr[right]);
return right;
}
  • 挖坑法

创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)

在这里插入图片描述

int _QuickSort2(int* arr, int left, int right)
{
int hole = left;
int key = arr[hole];

while (left < right)
{
while (left<right && arr[right]>key)
{
--right;
}
arr[hole] = arr[right];
hole = right;

while (left < right && arr[left] < key)
{
++left;
}
arr[hole] = arr[left];
hole = left;

}
arr[hole] = key;
return hole;
}
  • lomuto前后指针法

    创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。

在这里插入图片描述

int _QuickSort3(int* arr, int left, int right)
{
int prev = left, cur = left + 1;
int keyi = left;

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

return prev;
}

原文地址:https://blog.csdn.net/zoelinkailuo/article/details/142931064

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!