【数据结构】从0开始数据结构学习-内部排序

本节为内部排序相关内容,回到总目录:点击此处

排序的基本概念

内部排序: 排序过程完全在内存中进行
外部排序: 待排序记录数据量太大,内存无法容纳全部数据,排序需借助外部存储设备才能完成。

排序的稳定性: 假设待排序的序列中存在多个具有相同关键字的记录。若在排序前的序列Ri 领先于 Rj ,经过排序后得到的序列中的Ri仍然领先于Rj,则称所用的排序方法是稳定的。【看相同元素的领先关系是否发生了改变】

证明一种排序方式是稳定的,要通过算法本身的步骤加以证明。但证明排序方法是不稳定的,则只需要给出一个反例即可

插入类排序

在一个已排好序的记录子集的基础上,每一步将下一步待排序的记录有序插入到已排好序的记录子集中,直到所有待排记录全部插入为止。

直接插入排序

假设前面一段是有序序列
img

void InsSort(RecordType r[], int length)
{
    //r[0]是监视哨:
    //监视哨的作用:1、备份待插入的记录,以便前面关键字较大的记录后移
    		//2、防止越界
    for(i = 2; i <= length; i ++)
    {
        r[0] = r[i];
        j = i - 1;
        while(r[0].key < r[j].key && "j >= 0")
        {
            r[j+1] = r[j];
            j = j - 1;
        }
        r[j + 1] = r[0];
    }
}

时间复杂度: T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)
空间复杂度: S ( n ) = O ( 1 ) S(n) = O(1) S(n)=O(1) r[0]
算法是稳定的(因为插入的时候待插入的元素比较是从后面向前进行的,后面出现的关键字不可能插入到与前面相同的关键字之前)

折半插入排序
void Binsort(RecordType r[], int length)
{
    for(i = 2; i <= length; i ++)
    {
        x = r[i];
        low = 1;
        high = i -1;
        while(low <= high)//确定插入位置
        {
            mid = (low + high) / 2;
            if(x.key < r[mid].key) high = mid - 1;
            else low = mid + 1;
        }
        for(j = i - 1;j >= low; j --) r[j+1] = r[j];//元素依次向后移动
        r[low] = x;//插入元素
    }
}

只是减少了关键字间的比较次数,而记录的移动次数没有进行优化,所以该算法的时间 复杂度仍是 O(n2 )

希尔排序

希尔排序又称缩小增量排序法,是一种基于插入思想的排序方法,它利用了直接插入排序的最佳性质。
首先,将待排序的关键字序列分成若干个较小的子序列,对子序列进行直接插入排序,使整个待排序序列排好序。

image-20210613210022566

算法思想

先将待排序记录序列分割成若干个“较稀疏”的子序列,分别进行直接插入排序。经过上述粗略调整,整个序列中的记录已经基本有序,最后再对全部记录进行一次直接插入排序:
① 首先选定记录间的距离为 d i d_i di(i=1),在整个待排序记录序列中将所有间隔为 d 1 d_1 d1的记录分成一组,进行组内直接插入排序
② 然后取i=i+1,记录间的距离为 d i d_i di( d i < d i − 1 d_i<d_{i-1} di<di1),在整个待排序记录序列中,将所有间隔为 d i d_i di的记录分成一组,进行组内直接插入排序
重复步骤②,指导记录间的距离 d 1 = 1 d_1 = 1 d1=1,此时整个只有一个子序列,对该序列进行直接插入排序,完成整个排序过程。

//希尔排序ShellSort
void ShellSort(int r[], int len, int dist)
{
    int i,j;
    for(i = 1 + dist; i <= len; i ++)
    {
        if(r[i] < r[i - dist])
        {
            r[0] = r[i];
            for(j = i - dist; j > 0 && r[0] < r[j]; j -= dist)
            {
                r[j + dist] = r[j];
            }
            r[j + dist] = r[0];
        }
    }
}

//下标从0开始 使用vector
void ShellSort(vector<int> vec)
{
    for(int gap = vec.size()/2; gap > 0; gap /= 2)  //希尔排序增量为d/2
    {
        for(int i = gap;i < vec.size(); i ++)
        {
            int j = i; 
            while(j - gap >= 0 && vec[j-gap] > vec[j]) //直接插入排序
            {
                vec[j - gap] = vec[j - gap] + vec[j];
                vec[j]     = vec[j - gap] - vec[j];
                vec[j - gap] = vec[j - gap] - vec[j];
                j = j - gap;
            }
        }
    }
}

【增量的取法】

d = [d/2] --> 直到 d == 1;为止 缺点是奇数位置的元素在最后一部才会与偶数位置的元素进行比较

时间复杂度:希尔排序是最佳的插入排序,能够迅速的减少逆转序数。

算法稳定性:因为希尔排序会改变相同关键字的领先关系,因此希尔排序是不稳定的

小结
交换类排序

通过一系列交换逆序元素进行排序的方法

冒泡排序

每次将未排序中的最大的值找出来。

void BubbleSort(RecortTye r[], int n)
{
    change = True;
    //这里是假设是最后一个位置已排好
    //i = 1 最后已排好一个 只需要再比较n - i个所以j那里 j < n - i
    for(i = 1; i < n && change; i ++)
    {
        //change = false;
        for(j = 0; j < n-i; j ++)
        {
            if(r[j].key > r[j+1].key)
            {
                x = r[j];
                r[j] = r[j+1];
                r[j+1] = x;
                //change = true;
            }
        }
    }
}

时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度 O ( 1 ) O(1) O(1)
冒泡排序是稳定的

快速排序

冒泡排序只对相邻元素进行比较,因此每次只能消除一个逆序情况。快速排序可以交换两个不相邻的元素,从而达到消除多个逆序的作用

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return; //如果左侧大于等于右侧,无需再排序 //递归出口

    int i = l - 1; //左侧指针
    int j = r + 1; //右侧指针
    int x = q[l + r >> 1]; //随便取得分界点
    while (i < j)  //左侧指针的仍在左侧
    {
        do i ++ ; while (q[i] < x); //左侧指针指的数都小于x。否则停止指针活动
        do j -- ; while (q[j] > x); //右侧指针指的数都大于x。否则停止指针活动
        if (i < j) swap(q[i], q[j]); //如果左侧指针下标仍小于右侧指针下标,则交换两个数
    }
    quick_sort(q, l, j); //递归处理左右两边
    quick_sort(q, j + 1, r); //递归处理左右两边
}
void QKsort(RecordType r[], int low, int high)
{
    if(low < high)
    {
        pos = QKPass(r,low,high);
        QKsort(r,low,pos - 1);
        Qksort(r,pos+1, high);
    }
}
int QKPass(RecordType r[], int low, int high)
{
    x = r[low];
    while(low < high)
    {
        while(low < high && r[high].key >= x.key)
            high--;
        if(low < high)
        {
            r[low] = r[high];
            low++;
        }
        while(low < high && r[low].key < x.key)
            low++;
        if(low < high)
        {
            r[high] = r[low];
            high--;
        }
    }
    r[low] = x;
    return low;
}

时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 其最坏的情况是已经排好序的情况 O ( n 2 ) O(n^2) O(n2)
空间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n) //递归栈
快速排序是不稳定的

小结
选择类排序

每一趟在n-i+1个记录中选取关键字最小的记录作为有序序列中第i个记录。

简单选择排序

该算法的实现思想为:对于具有 n 个记录的无序表遍历 n-1 次,第 i 次从无序表中第 i 个记录开始,找出后序关键字中最小的记录,然 后放置在第 i 的位置上。

image-20210616212050382<—过程

void SelectSort(RecordType r[], int n)
{
    for(i = 1; i <= n - 1; i ++)
    {
        k = i;
        for(j = i + 1;j <= n; j ++)
        {
            if(r[j].key < r[k].key) k = j;
        }
        if(k != i)
        {
            x = r[i];
            r[i] = r[k];
            r[k] = x;
        }
    }
}

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

树形选择排序

相比的改进是在排序比较过程中记录元素大小关系

树形选择排序也叫锦标赛排序:基本思想是先把待排序的n个记录的关键字两两进行比较,取出较小者,然后在[n/2]个较小者中,采用同样的方法进行比较,选出每两个中的较小者,如此反复,直到选出最小关键字记录为止。

过程:

image-20210616212354038
void TreeSelectionSort(int *mData)
{
    int MinValue = 0;
    int tree[N * 4]; //树的大小
    int max,maxIndex,treeSize;
    int i,n = N, baseSize = 1;
    while(baseSize < n)
        baseSize *= 2;
    treeSize = baseSize * 2 - 1;
    for(i = 0; i < n; i ++) //将要排的数字填到树上
    {
        tree[treeSize - i] = mData[i];
    }
    for(;i < baseSize; i ++) //其他地方填最小值
    {
        tree[treeSize - i] = MinValue; 
    }
    //构造一棵树,大的值向上构造
    for(i = treeSize; i > 1; i -= 2)
    {
        tree[i / 2] = (tree[i] > tree[i-1] ? tree[i] : tree[i - 1]);
    }
    n -= 1;
    //每次取出最大值(根节点所在位置)取到原数组的最后一个位置,然后把最大值改成最小值
    while(n != -1)
    {
        max = tree[1]; //树顶为最大值
        mData[n--] = max; //从大到小倒着贴到原数组上
        maxIndex = treeSize; //最大值下标
        while (tree[maxIndex] != max)
        {
            maxIndex --; 
        }//找最大值下标
        tree[maxIndex] = MinValue;
        while(maxIndex > 1)
        {
            if(maxIndex % 2 == 0)
            {
                tree[maxIndex / 2] = (tree[maxIndex] > tree[maxIndex + 1] ? tree[maxIndex]:tree[maxIndex + 1]);
            }
            else
            {
				tree[maxIndex / 2] = (tree[maxIndex] > tree[maxIndex - 1] ? tree[maxIndex] : tree[maxIndex - 1]);
            }
            maxIndex /= 2;
        }
    }
}

时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
空间复杂度: O ( n ) O(n) O(n)

堆排序

改进方法:将存储在向量中的数据元素看成一棵完全二叉树,减少了辅助空间

将向量中存储的数据看成一棵完全二叉树,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择关键字最小的记录,即排序记录仍采用向量数组方式存储,并非采用树的存储结构,而仅仅是采用完全二叉树的顺序结构的特征进行分析

通过将无序表转化为堆,可以直接找到表中最大值或者最小值,然后将其提取出来,令剩余的记录再重建一个堆,取出次大值或者次小 值,如此反复执行就可以得到一个有序序列,此过程为堆排序。

把待排序的记录的关键字存放在数组r[1…n]中,将r看成是一棵完全二叉树的顺序表示,每一个节点表示一个记录,第一个记录r[1]作为二叉树的树根,以下各记录r[2]~r[n]依次逐层从左到右顺序排列,任意结点r[i]的左孩子是r[2i],右孩子是r[2i+1],双亲结点时r[i/2]。

如果下标从0开始的话,双亲 r[(i - 1) / 2]

大根堆: 关键字的结点要大于等于左右两个孩子的结点
小根堆: 关键字的结点要小于等于左右两个孩子的结点

堆排序的过程主要需要解决两个问题:1、按堆定义建初堆;2、去掉最大元后重建堆得到次大元

提示:堆用完全二叉树表示时,其表示方法不唯一,但是可以确定的是树的根结点要么是无序表中的最小值,要么是最大值。

//大根堆--递增;小根堆--递减
//重建堆 保证根节点是最大值
void heapify(int tree[], int n, int i)
{
    if ( i >= n ) return;  //递归出口
    int c1 = 2 * i + 1;//第一个子节点
    int c2 = 2 * i + 2; //第二个子节点
    int max = i; //假设i是最大值
   	//寻找最大值
    //注意一下要保证c1,c2不会出界
    if(c1 < n && tree[c1] > tree[max])
    {
		max = c1;
    }
    if(c2 < n && tree[c2] > tree[max])
    {
        max = c2;
    }
    if (max != i)
    {
        swap(tree[max], tree[i]);
        heapify(tree, n, max);
    }
}
//以上堆排序过程运行的前提是已经是堆才行

//建初堆
//原来的数组从n-1层开始对每个结点进行一次heapify操作
void build_heap(int tree[], int n)
{
    int last_node = n - 1; //获取最后一个结点
    int parent = (last_node - 1) / 2; //获取最后一个结点的父节点
    for(int i = parent; i >= 0; i --) //对每一个结点都进行一次堆排序操作,即可将一个无序序列变成一个堆序列
    {
        heapify(tree, n, i);
    }
}

//堆排序
void heap_sort(int tree[], int n)
{
    build_heap(tree, n);
    for(int i = n - 1; i >= 0; i --)
    {
        swap(tree[i], tree[0]);
        heapify(tree, i, 0);  //i 代表当前树的结点的树
        //因为每次去掉了一个点,所以长度也会变化,实际上长度就是i
    }
}

时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
空间复杂度: O ( 1 ) O(1) O(1)

归并排序

基本思想是基于合并,将两个或两个以上有序表合并成一个新的有序表。

分治

提示:归并排序算法在具体实现时,首先需要将整个记录表进行折半分解,直到分解为一个记录作为单独的一张表为止,然后在进行两 两合并。整个过程为分而后立的过程。

1、将序列中待排序数字分为若干组,每个数字分为一组
2、将若干个组两两合并,保证合并后的组是有序的
3、重复第二步操作直到只剩下一组,排序完成

void merge_sort(int q[], int l, int r)
{
    if (l>=r) return; //当区间里面的个数是一个或者没有的话则退出 
    int mid = l+r >>1;
    
    merge_sort(q,l,mid);
    merge_sort(q,mid+1,r);  //递归处理左右两边
    
    int k = 0, i = l, j = mid +1;
    //k表示已经归并排序的个数
    //归并排序需要另外开一个数组来储存数据temp
    while(i <= mid && j<=r)
        if (q[i] <= q[j]) tmp[k++] = q[i++];
    	else temp[k++] = q[j++];
    while (i <= mid) tmp[k++] = q[i++]; //如果左边还没有归并完,则直接把左边部分的数移动到原来数组后面
    while (j <= r) tmp[k++] = q[j++];  //如果右边还没有归并完,则直接把右边部分的数据移动到原来数组后面
    
    for (i = l,j = 0;i <= r;i ++,j ++) q[i] = tmp[j]; //最后需要把处理后的数据返回到原来的数组内
}

归并排序算法的时间复杂度为 O(nlogn)。该算法相比于堆排序和快速排序,
其主要的优点是:当记录表中含有值相同的记录时,排序前和排序后在表中的相对位置不会改变。

分配类排序
多关键字排序

第一种方法:排序再分配;高位优先

第二种方法:分配和收集交替进行

基数排序

基数排序属于上述“低位优先”排序法,通过反复进行分配与收集操作完成排序。

基数排序不同于之前所介绍的各类排序,前边介绍到的排序方法或多或少的是通过使用比较和移动记录来实现排序,而基数排序的实现 不需要进行对关键字的比较,只需要对关键字进行“分配”与“收集”两种操作即可完成。

按照个位-十位-百位的顺序进行基数排序,此种方法是从最低位开始排序——>最低位优先法(LSD法)
按照百位-十位-各位的顺序进行排序,称为最高位优先法(简称“MSD 法”),使用该方式进行排序同最低位优先法不同的是:当无序表中的关键字进行分配后,相当于进入了多个子序列,后序的排序工作分别在各个子序列中进行(最低位优先法每次分配与收集都是相对于整个序列表而言的)。

最高位优先法完成排序的标准为:直到每个子序列中只有一个关键字为止

image-20210617190954563
#define RADIX 10
#define KEY_SIZE 6
#define LIST_SIZE 20
typedef int KeyType;
typedef struct
{
    KeyType keys[KEY_SIZE];
    OtherType other_data;
    int next;    //静态链域
}RecordType;
typedef struct
{
    RecordType r[LIST_SIZE  + 1]; //r[0]为头结点
    int length;
    int keynum;
}SLinkList; //静态链表
typedef int PVector[RADIX];

void Distribute(RecordType r[], int i, PVector head, PVector tail)
{
    //记录数组r中记录已按低位关键字key[i+1]...key[d]进行过低位优先排序,
}

void Collect(RecordType r[], PVector hear, PVector tail)
{

}

void RadixSort(RecordType r[], int n, int d)
{
    
}

参考:https://www.cnblogs.com/skywang12345/p/3603669.html

//获取最大元素
int get_max(int a[], int n)
{
    int i, max;

    max = a[0];
    for (i = 1; i < n; i++)
        if (a[i] > max)
            max = a[i];
    return max;
}
/*
 * 对数组按照"某个位数"进行排序(桶排序)
 *
 * 参数说明:
 *     a -- 数组
 *     n -- 数组长度
 *     exp -- 指数。对数组a按照该指数进行排序。
 *
 * 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};
 *    (01) 当exp=1表示按照"个位"对数组a进行排序
 *    (02) 当exp=10表示按照"十位"对数组a进行排序
 *    (03) 当exp=100表示按照"百位"对数组a进行排序
 *    ...
 */
void count_sort(int a[], int n, int exp)
{
    int output[n];             // 存储"被排序数据"的临时数组
    int i, buckets[10] = {0};

    // 将数据出现的次数存储在buckets[]中
    for (i = 0; i < n; i++)
        buckets[ (a[i]/exp)%10 ]++;

    // 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。
    for (i = 1; i < 10; i++)
        buckets[i] += buckets[i - 1];

    // 将数据存储到临时数组output[]中
    for (i = n - 1; i >= 0; i--)
    {
        output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
        buckets[ (a[i]/exp)%10 ]--;
    }

    // 将排序好的数据赋值给a[]
    for (i = 0; i < n; i++)
        a[i] = output[i];
}

//基数排序
void radix_sort(int a[], int n)
{
    int exp;    // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;...
    int max = get_max(a, n);    // 数组a中的最大值

    // 从个位开始,对数组a按"指数"进行排序
    for (exp = 1; max/exp > 0; exp *= 10)
        count_sort(a, n, exp);
}
  1. 首先通过get_max(a)获取数组a中的最大值。获取最大值的目的是计算出数组a的最大指数。

  2. 获取到数组a中的最大指数之后,再从指数1开始,根据位数对数组a中的元素进行排序。排序的时候采用了桶排序。

  3. count_sort(a, n, exp)的作用是对数组a按照指数exp进行排序。

排序方法的综合比较
image-20210617170611109

上表中的简单排序包含出希尔排序之外的所有插入排序,起泡排序和简单选择排序。同时表格中的 n 表示无序表中记录的数量;基数排 序中的 d 表示进行分配和收集的次数。

在上表表示的所有“简单排序算法”中,以直接插入排序算法最为简单,当无序表中的记录数量 n 较小时,选择该算法为最佳排序方法。

所有的排序算法中单就平均时间性能上分析,快速排序算法最佳,其运行所需的时间最短,但其在最坏的情况下的时间性能不如堆排序和归并排序;堆排序和归并排序相比较,当无序表中记录的数量 n 较大时,归并排序所需时间比堆排序短,但是在运行过程中所需的辅助存储空间更多以空间换时间)。

从基数排序的时间复杂度上分析,该算法最适用于对 n 值很大但是关键字较小的序列进行排序

在所有基于“比较”实现的排序算法中(以上排序算法中除了基数排序,都是基于“比较”实现),其在最坏情况下能达到的最好的时间复杂度为 O(nlogn)

稳定性 :本章所介绍的所有排序算法中,选择排序、快速排序和希尔排序都不是稳定的排序算法;而冒泡排序、插入排序、归并排序和基数排序 都是稳定的排序算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值