数据结构之排序(1)——插入排序

本文详细介绍了三种插入排序算法:直接插入排序、折半插入排序及希尔排序。每种算法都提供了核心思想、时间复杂度分析及代码实现。此外,还探讨了在结构体排序中的应用,以及希尔排序的增量序列对效率的影响。

插入排序主要包括三个:直接插入排序、折半插入排序和希尔排序。基本思想都是:

每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中,直到全部记录插入完成。

下面依次进行讲解并编程实现。

直接插入排序

主要思想:
1. 循环i从1~n-1,每次循环执行步骤2-4。
2. 查找arr[i]arr[0]~arr[i-1]的插入位置k。
3. 将arr[k]~arr[i-1]每个元素后移一个位置。
4. 将arr[i]复制到arr[k]

如图所示:
这里写图片描述

时间复杂度:

  • 最好情况下,每次循环只需和arr[i-1]比较,并不用移动,为 O(n)
  • 最差情况下,下标为i的元素要和前面i个元素都比较一下,则一共比较次数为 n1i=1i ,即 (n2n)/2 次;移动次数也最大,为 n1i=1i+1 ,因为还有基准元素赋到第一位,所以多了1,复杂度为 O(n2)

平均下来,约为 ((n2+n)/4) 次,复杂度为 O(n2)
稳定性:稳定
适用性:用顺序表时,从后往前比较,比较后直接交换;单链表的话,从前往后查找。

不带哨兵

核心代码如下:

void insertSort(int arr[], int n)
{
    int i, j;
    int tmp;
    for (i = 1; i < n; ++i){
        if (arr[i] < arr[i - 1]){
            tmp = arr[i];
            for (j = i - 1; tmp < arr[j] && j>=0; --j)
                arr[j + 1] = arr[j];
            arr[j + 1] = tmp;
        }
    }
}

带哨兵

更一般的,可以引入哨兵arr[0],(哨兵:简化边界条件而引入的附加结点)有两个作用:
1. 可以减少临界的比较,即不用比较j>=0
2. 暂时存放待插入的元素

如下:

void insertSort(int arr[], int n)//这里n是待排序数个数,不包括哨兵
{
    int i, j;
    for (i = 2; i <= n; ++i){
        if (arr[i] < arr[i - 1]){
            arr[0] = arr[i];
            for (j = i - 1; arr[0] < arr[j]; --j)
                arr[j + 1] = arr[j];
            arr[j + 1] = arr[0];
        }
    }
}

拓展:元素为结构时

如果待排序的是结构的话,也一样:

#include <iostream>
using namespace std;

struct Elem
{
    int key;
    Elem(){}
    Elem(int key) :key(key){}
    bool operator < (const Elem& other)
    {
        return this->key < other.key;//升序排列
    }
    Elem& operator = (const Elem& other)
    {
        this->key = other.key;
        return *this;
    }
    friend ostream& operator << (ostream &stream, const Elem &e)
    {
        stream << e.key;
        return stream;
    }
};
void print(Elem arr[], int n, int i)
{
    cout << "第" << i << "轮:";
    for (int i = 0; i < n; ++i){
        cout << " " << arr[i];
    }
    cout << endl;
}
void insertSort(Elem arr[], int n)
{
    int i, j;
    for (i = 2; i <= n; ++i){
        if (arr[i] < arr[i - 1]){
            arr[0] = arr[i];
            for (j = i - 1; arr[0] < arr[j]; --j)
                arr[j + 1] = arr[j];
            arr[j + 1] = arr[0];
        }
        print(arr, n+1, i-1);
    }
}
int main()
{
    Elem arr[9] = { { 0 },{ 6 }, { 5 }, { 3 }, { 1 }, { 8 }, { 7 }, { 2 }, { 4 } };
    print(arr, 9, 0);
    insertSort(arr, 8);
    system("pause");
}

第0轮: 0 6 5 3 1 8 7 2 4
第1轮: 5 5 6 3 1 8 7 2 4
第2轮: 3 3 5 6 1 8 7 2 4
第3轮: 1 1 3 5 6 8 7 2 4
第4轮: 1 1 3 5 6 8 7 2 4
第5轮: 7 1 3 5 6 7 8 2 4
第6轮: 2 1 2 3 5 6 7 8 4
第7轮: 4 1 2 3 4 5 6 7 8

折半插入排序

思路是:由于前一部分是已排序的,所以不再一次次比较,而是二分查找到待插入元素应该在哪个位子,再插入,后移操作统一放到一起。

void insertSort(int arr[], int n)
{
    int i, j, low, mid, high;
    for (i = 2; i <= n; ++i){
        arr[0] = arr[i];
        low = 1; high = i - 1;
        while (low <= high){
            mid = (low + high) / 2;
            if (arr[mid] > arr[0]) 
                high = mid - 1;
            else low = mid + 1;//相等的话,插到后面,算法稳定
        }
        for (j = i - 1; j>=high+1 ; --j)//high+1到i-1的数后移
            arr[j + 1] = arr[j];
        arr[high + 1] = arr[0];//待插入数放到high+1
    }
}

用了二分查找仅减少了比较的复杂度,从 O(n2) O(nlog2n) ,且次数与数列初始状态无关,仅取决于个数n,即最好情况最差情况都是这么多。
移动次数仍然一样,但移动次数与数列初始状态有关,最好情况为0,最差情况复杂度为 O(n2)

故总的来说,时间复杂度

  • 最好情况为 O(nlog2n) ,仅用在查找,不移动。
  • 最差情况为 O(n2) ,移动+查找。

平均为 O(n2)
稳定性:稳定。
空间复杂度: O(1)

希尔排序

基本思路:先将待排序表分割成若干个形如 L[i,i+d,i+2d,...i+kd] 的特殊子表,分别进行直接插入排序,随着 d 不断减小至1,整个表也会趋向于“基本有序”,最后d为1时,进行一次整体的直接插入排序。

和直接插入相比,算法修改为:
1. 前后比较增量为dk,不是1。
2. arr[0]不再是哨兵,而仅作为暂时存储用,即判断越界用不到了。

void shellSort(int arr[], int n)
{   
    int dk, i, j;//dk为增量
    for (dk = n / 2; dk >= 1; dk = dk / 2){
        for (i = dk + 1; i <= n; ++i){
            if (arr[i] < arr[i - dk]){
                arr[0] = arr[i];
                for (j = i - dk; j>0 && arr[0]<arr[j]; j -= dk)
                    arr[j + dk] = arr[j];
                arr[j + dk] = arr[0];
            }
        }
    }
}

空间复杂度:用了3个辅助单元,为 O(1)
时间复杂度:依赖于增量序列的函数。
稳定性:不稳定。
适用性:仅适用于顺序存储的线性表。(因为增量不为1,要能以常数时间访问到任何一个元素,链式就不行了。)

各算法复杂度表

算法平均时间复杂度最好时间复杂度最差时间复杂度空间复杂度稳定性备注
直接插入排序 O(n2) O(n) O(n2) O(1) 稳定
折半插入排序 O(n2) O(nlog2n) O(n2) O(1) 稳定
shell排序 O(n1.3) O(n) O(n2) O(1) 不稳定和增量序列有关
实验1顺序表基本操作 一、实验目的 1.学会定义线性表的顺序存储类型,实现C程序的基本结构,对线性表的一些基本操作和具体的函数定义。 2.掌握顺序表的基本操作,实现顺序表的插入、删除、查找以及求并集等运算。 3.掌握对多函数程序的输入、编辑、调试和运行过程。 二、实验要求 1.预习C语言中结构体的定义与基本操作方法。 2.对顺序表的每个基本操作用单独的函数实现。 3.编写完整程序完成下面的实验内容并上机运行。 4.整理并上交实验报告。 三、实验内容: 1.编写程序实现顺序表的下列基本操作: (1)初始化顺序表La。 (2)将La置为空表。 (3)销毁La。 (4)在La中插入一个新的元素。 (5)删除La中的某一元素。 (6)在La中查找某元素,若找到,则返回它在La中第一次出现的位置,否则返回0。 (7)打印输出La中的元素值。 2.编写程序完成下面的操作: (1)构造两个顺序线性表La和Lb,其元素都按值非递减顺序排列。 (2)实现归并La和Lb得到新的顺序表Lc,Lc的元素也按值非递减顺序排列。 (3)假设两个顺序线性表La和Lb分别表示两个集合A和B,利用 union_Sq操作实现A=A∪B。 四、思考与提高 假设两个顺序线性表La和Lb分别表示两个集合A和B,如何实现A=A ∩B ? 实验2:单链表基本操作 一、 实验目的 1. 学会定义单链表的结点类型,实现对单链表的一些基本操作和具体的函数定义,了解并掌握单链表的类定义以及成员函数的定义与调用。 2. 掌握单链表基本操作及两个有序表归并、单链表逆置等操作的实现。 二 、实验要求 1.预习C语言中结构体的定义与基本操作方法。 2.对单链表的每个基本操作用单独的函数实现。 3.编写完整程序完成下面的实验内容并上机运行。 4.整理并上交实验报告。 三、实验内容 1.编写程序完成单链表的下列基本操作: (1)初始化单链表La。 (2)在La中第i个元素之前插入一个新结点。 (3)删除La中的第i个元素结点。 (4)在La中查找某结点并返回其位置。 (5)打印输出La中的结点元素值。 2 .构造两个带有表头结点的有序单链表La、Lb,编写程序实现将La、Lb合并成一个有序单链表Lc。 合并思想是:程序需要3个指针:pa、pb、pc,其中pa,pb分别指向La表与Lb表中当前待比较插入的结点,pc 指向Lc表中当前最后一个结点。依次扫描La和Lb中的元素,比较当前元素的值,将较小者链接到*pc之后,如此重复直到La或Lb结束为止,再将另一个链表余下的内容链接到pc所指的结点之后。 3.构造一个单链表L,其头结点指针为head,编写程序实现将L逆置。(即最后一个结点变成第一个结点,原来倒数第二个结点变成第二个结点,如此等等。) 四、思考与提高 1.如果上面实验内容2中合并的表内不允许有重复的数据该如何操作? 2.如何将一个带头结点的单链表La分解成两个同样结构的单链表Lb,Lc,使得Lb中只含La表中奇数结点,Lc中含有La表的偶数结点? 实验3:循环队列基本操作 一 、实验目的 1.熟悉并能实现循环队列的定义和基本操作。 2.了解用队列解决实际应用问题。 二、实验要求 1.进行队列的基本操作时要注意队列“先进先出”的特性。 2.复习关于队列操作的基础知识。 3.编写完整程序完成下面的实验内容并上机运行。 4.整理并上交实验报告。 三、实验内容 1.任意输入队列长度和队列中的元素值,构造一个顺序循环队列,对其进行清空、插入新元素、返回队头元素以及删除队头元素操作。 2.约瑟夫环的实现:设有n个人围坐在圆桌周围,现从某个位置 i 上的人开始报数,数到 m 的人就站出来。下一个人,即原来的第m+1个位置上的人,又从1开始报数,再是数到m的人站出来。依次重复下去,直到全部的人都站出来,按出列的先后又可得到一个新的序列。由于该问题是由古罗马著名的史学家Josephus提出的问题演变而来,所以通常称为Josephus 问题。 例如:当n=8,m=4,i=1时,得到的新序列为: 4,8,5,2,1,3,7,6 编写程序选择循环队列作为存储结构模拟整个过程,并依次输出出列的各人的编号。 实验4:矩阵的压缩存储及相关操作 (11周星期三7、8节) 一 、实验目的 1.掌握下三角矩阵的输入、输出、转置算法。 2.理解稀疏矩阵的三元组表类型定义,掌握稀疏矩阵的输入、输出、转置算法。 二 、实验要求 1.认真阅读和掌握本实验的算法思想。 2.编写完整程序完成下面的实验内容并上机运行。 三、实验内容 1.所谓上(下)三角矩阵是指矩阵的下(上)三角中的元素均为常数或零的n阶矩阵。此时除了存储上(下)三角矩阵中的元素之外再加一个存储常数的空间即可。三角矩阵中的重复元素c可共享一个存储空间,其余的元素正好有n×(n+1)/2个,因此,三角矩阵可压缩到向量Sa[0……n×(n+1)/2]中,其中c存放在向量的最后一个分量中。用向量Sa[0……n×(n+1)/2]压缩存储下三角矩阵,编写程序任意输入一个下三角矩阵,对其进行转置,输出转置后的矩阵。 2.用三元组顺序表压缩存储稀疏矩阵,编写程序任意输入一个稀疏矩阵,对其进行转置,输出转置后的矩阵。 四、思考与提高 如何计算一个三元组表表示的稀疏矩阵对角线元素之和以及两个三元组表表示的稀疏矩阵的乘积? 实验5:二叉树的建立及遍历 (第十三周星期三7、8节) 一 、实验目的 1.学会实现二叉树结点结构和对二叉树的基本操作。 2.掌握对二叉树每种操作的具体实现,学会利用递归方法编写对二叉树这种递归数据结构进行处理的算法。 二 、实验要求 1.认真阅读和掌握和本实验相关的教材内容。 2.编写完整程序完成下面的实验内容并上机运行。 3.整理并上交实验报告。 三、实验内容 1.编写程序任意输入二叉树的结点个数和结点值,构造一棵二叉树,采用三种递归遍历算法(前序、中序、后序)对这棵二叉树进行遍历并计算出二叉树的高度。 2 .编写程序生成下面所示的二叉树,并采用中序遍历的非递归算法对此二叉树进行遍历 四、思考与提高 1.如何计算二叉链表存储的二叉树中度数为1的结点数? 2.已知有—棵以二叉链表存储的二叉树,root指向根结点,p指向二叉树中任一结点,如何求从根结点到p所指结点之间的路径? 实验6:二分查找、Hash查找算法的程序实现 (第十五三周星期三7、8节) 一、 实验目的 1 .熟练掌握二分查找算法并能在有序表中进行查找操作。 2. 掌握Hash表的相关算法。 二 、实验要求 1.认真阅读和掌握和本实验相关的教材内容。 2.复习顺序表及二叉树的基本操作过程。 3.编写完整程序完成下面的实验内容并上机运行。 4.整理并上交实验报告。 三、实验内容 1.二分查找又称为折半查找,它要求要查找的顺序表必须是有序表,即表中结点按关键字有序.并且要用顺序存储结构。 基本思想是:首先将给定值key与表中中间位置记录的关键字相比较,若二者相等,则查找成功,否则根据比较的结果确定下次查找的范围是在中间记录的前半部分还是后半部分,然后在新的查找范围内进行同样的查找,如此重复下去,直到在表中找到关键字与给定值相等的记录,或者确定表中没有这样的记录。 编写程序构造一个有序表La,从键盘接收一个关键字key,用二分查找法在La 中查找key,若找到则提示查找成功并输出key所在的位置,否则提示没有找到信息。 2.编写程序实现Hash表的建立、删除、插入以及查找操作。 程序应包含的主要功能函数有: Hash( ):计算哈希地址 InitialHash( ):初始化哈希表 SearchHash( ):在哈希表中查找关键字 InsertHash( ):向哈希表中插入关键字 DeleteHash( ):删除哈希表中某一关键字 PrintHash ( ):打印输出哈希表 四、思考与提高 如何利用二分查找算法在一个有序表中插入一个元素x,并保持表的有序性? 实验7:至少三种排序算法的程序实现 (第十六周星期三7、8节) 一、 实验目的 1.掌握简单插入排序、冒泡排序、快速排序、堆排序以及归并排序的算法并加以应用。 2.对各种查找、排序技术的时间、空间复杂性有进一步认识。 二 、实验要求 1.认真阅读和掌握和本实验相关的教材内容。 2.编写完整程序完成下面的实验内容并上机运行。 3.整理并上交实验报告。 三、实验内容 编写程序实现下述五种算法至少三种,并用以下无序序列加以验证: 49,38,65,97,76,13,27,49 1.简单插入排序 2.冒泡排序 3.快速排序 4.归并排序 5.堆排序 四、思考与提高 1.设有1000个无序的元素,希望用最快的速度挑出其中前10个最大的元素,采用哪一种排序方法最好?为什么? 2.如何构造一种排序方法,使五个整数至多用七次比较就可以完成排序任务? 实验8:集成实验 一、 实验目的 目的:扩大编程量,完成模块化程序设计的全过程。 二 、实验要求 1.认真阅读和掌握和本实验相关的教材内容。 2.编写完整程序完成下面的实验内容并上机运行。 3.整理并上交实验报告。 三、实验内容 将已经实现的模块装配在一起,由菜单进行管理,形成一个小型多功能软件。 while(1){ menuList(); scanf(&n); switch(n){ case 1:……;break; case 2:……;break; . . case 0:return; } } 如: • MenuSelect( ){ int select; SqList A , B , C ; MenuList( ); /* 打印菜单 */ scanf(“%d”,&select;); switch(select){ case 1: InitList_Sq( &A ); break; case 2: InitList_Sq( &B ); break; case 3: Insert(&A);break; case 4: Insert(&B );break; case 5: Union(&A ,B);break; ………….. default: printf(“\n ERROR\n”); } } 四、思考与提高 1. 优化已完成的程序,使整个实验形成至少三级菜单管理; 2.美化输出界面,使得操作友好。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值