(端午特别版)【C语言 - 数据结构】排序(上)

简介: (端午特别版)【C语言 - 数据结构】排序(上)

一、排序的概念及其运用


1.1排序的概念


 排序的严格定义:假设含有 个记录的序列为{r1,r2. ... ... .rn}其相应的别为 ,其相应的关键字分别为{k1, k2, ……kn},需确定1,2,……n 的一种排列p1, p2, ……pn,使其相应的关键字满足 kpl<=kp2<=……<=kpn(非递减或者非递增)关系,即使得序列成为一个按关键字的序列(rp1, rp2,……rpn),这样的操作就称为排序。


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


啥是稳定性,稳定性的概念:


 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。


内部排序和外部排序的概念:


内部排序:数据元素全部放在内存中的排序。


外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。


对于内排序来说,排序算法的性能主要是受三个方面影响:


1.时间性能:排序是数据处理中经常执行的一种操作,往往属于系统的核心部分,因此排序算法的时间开销是衡量其好坏的最重要的标志。在内排序中,主要进行两种操作:比较和移动 比较指关键字之间的比较,这是要做排序最起码的操作。 移动指记录从一个 位置移动到另一个位置,事实上,移动可以通过改变记录的存储方式来予以避免。


2.辅助空间:评价排序算法的另一个主要标准是执行算法所需要的辅助存储空间。辅助存储空 间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。


3.算法的复杂性:注意这里指的是算法本身的复杂度,而不是指算法的时间复杂度。显然算法过于复杂也会影响排序的性能。


1.2常见的排序算法


1669441029572.jpg


二、常见的排序算法及其实现


2.1冒泡排序


我想大家都对冒泡排序不陌生,毕竟这是许多初学者最开始学的一种排序算法,今天让我们剖析一些这个算法吧!

 

1669441054131.jpg


冒泡排序 Bubble Sort 一种交换排序,其的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。


冒泡排序的代码:

void BubbleSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
  int exchange = 0;//定义一个变量判断是否发生了交换,若没有交换则直接break出去
  for (int j = 0; j < n - i - 1; j++)
  {
    if (a[j] > a[j + 1])
    {
    exchange = 1;
    Swap(&a[j], &a[j + 1]);
    }
  }
  if (exchange == 0)//说明没有交换过
  {
    break;
  }
  }
}


如果是用C语言写的代码建议大家写一个Swap函数和PrintfArray输出数组的函数


Swap和PrintfArray函数代码:

void Swap(int* pa, int* pb)
{
  int tmp = *pa;
  *pa = *pb;
  *pb = tmp;
}
void PrintfArray(int* a, int n)
{
  int i = 0;
  for (i = 0; i < n; i++)
  {
  printf("%d ", a[i]);
  }
  printf("\n");
}

2.2插入排序


2.2.1插入排序的基本思想


直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。


2.2.2插入排序的操作:


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


2.2.3直接插入排序的特性总结:


1. 元素集合越接近有序,直接插入排序算法的时间效率越高


2. 时间复杂度:O(N^2)


3. 空间复杂度:O(1),它是一种稳定的排序算法


4. 稳定性:稳定


插入排序代码:

void InsertSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
  //[0, end] end + 1
  int end = i;
  //单趟排序; [0, end] 有序的。 end + 1位置的值,插入进入,保持他依旧有序
  int tmp = a[end + 1];
  while (end >= 0)
  {
    if (tmp < a[end])
    {
    a[end + 1] = a[end];//先交换
    --end;//往前
    }
    else
    {
    break;
    }
    a[end + 1] = tmp;//tmp的值改到了之前的end位置
  }
  }
}


冒泡排序和插入排序对比:


1669441137232.jpg


2.3堆排序


堆排序即利用堆的思想来进行排序,总共分为两个步骤:


1. 建堆 升序:建大堆


          .降序:建小堆


2. 利用堆删除思想来进行排序 建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。


书中的描述:


堆排序 (Heap Sort) 就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是, 将待排序的序列构造成一个大根堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值) .然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。


代码示例:(小根堆)

void AdjustDown(HPDataType* a, size_t size, size_t root)
{
       size_t parent = root;
       size_t child = parent * 2 + 1;//默认是左孩子
       while (child < size)
       {
              //1、选出左右孩子中小的那个,而且保证不会越界访问
              if (child + 1 < size && a[child + 1] <a[child])//建大堆时< 改 >
              {
                      ++child;//左孩子变为右孩子
              }
              if (a[child] < a[parent])//建大堆时< 改 >
              {
                      Swap(&a[child], &a[parent]);
                      parent = child;//继续计算
                      child = parent * 2 + 1;//默认还是计算左孩子
              }
              else
              {
                      break;
              }
       }
}
void HeapSort2(int* a, int n)
{
  //向上调整--建堆
  //for (int i = 1; i < n; ++i)
  //{
  //  AdjustUp(a, i);
  //}
  //向下调整--建堆O(N)
  for (int i = (n - 1 - 1) / 2; i >= 0; --i)
  {
  AdjustDown(a, n, i);//为什么向下调整要多传一个参数,因为当child>=size 时说明已经到了边界
  }
  size_t end = n - 1;//n - 1是最后一个数据的下标
  while (end > 0)
  {
  Swap(&a[0], &a[end]);
  AdjustDown(a, end, 0);
  //次大的数到了倒数第二个位置
  --end;
  }
}
int main()
{
//  TestHeap();
  int a[] = { 4 , 2, 7, 8, 5, 1, 0, 6 };
  HeapSort(a, sizeof(a) / sizeof(int));
  for (int i = 0; i < sizeof(a) / sizeof(int); i++)
  {
  printf("%d ", a[i]);
  }
  printf("\n");
  return 0;
}


2.4选择排序


2.4.1基本思想:


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


2.4.2直接选择排序:


1、在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素


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


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


2.4.3直接选择排序的特性总结:


1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用


2. 时间复杂度:O(N^2)


3. 空间复杂度:O(1)


4. 稳定性:不稳定


选择排序与堆排序对比:


选择排序在每选出一个数需要遍历N次


堆排序建好堆后只需logN


所以堆排序的时间复杂度是o(n*logN),而选择排序是o(n^2);


选择排序的代码:


思路:每一次选出最大最小值,最大放最后,最小放后面,再left++,right--缩小区间


void SelectSort(int* a, int n)
{
    int left = 0, right = n - 1;
    while (left < right)
    {
        int mini = left, maxi = left;
        for (int i = left + 1; i <= right; i++)
        {
            if (a[i] < a[mini])
            {
                mini = i;
            }
            if (a[i] > a[maxi])
            {
                maxi = i;
            }
        }
        Swap(&a[left], &a[mini]);
        //maxi可能与left重叠
        if (left == maxi)
        {
            maxi = mini;
        }
        Swap(&a[right], &a[maxi]);
        left++;
        right--;
    }
}


2.5希尔排序


希尔排序我们可以简单理解成是插入排序加强版,而且是史诗级加强。


2.5.1希尔排序的原理:


现在,我要讲解的算法叫希尔排序 (Shell Sort ,希尔排序是 DL.Shell 1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是 O(n^2)的,希尔排序算法是突破这个时间复杂度的第一批算法。


原理:


先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。


1669441222317.jpg


2.5.2希尔排序的特性总结:


1. 希尔排序是对直接插入排序的优化。


2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。


3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的 希尔排序的时间复杂度都不固定。


希尔排序代码:

void ShellSort(int* a, int n)
{
    //预排序
    int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        for (int i = 0; i < n - gap; i += gap)//为什么是到n - gap因为
        //结束前最后一次执行的下标是n - gap - 1
        {
            int end = i;
            int tmp = a[end + gap];
            while (end >= 0)
            {
                if (tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
                a[end + gap] = tmp;
            }
        }
    }
}


三、排序算法复杂度及稳定性分析


3.1算法的空间特征


·需要使用堆栈或者递归实现:快排和归并排序


·常数空间复杂度:堆排序、希尔、插入、冒泡、选择


3.2算法与数据结构的关系


数据不敏感:归并排序、选择排序、堆排序


数据敏感:冒泡、快排、插入、希尔。


3.3算法的分类:


分治:归并和快排


基于插入:希尔、直接插入、选择


基于选择:堆排序、直接选择


3.4算法的稳定性:


稳定算法:归并、插入、冒泡


不稳定:堆排序、快排、希尔、选择


1669441268965.jpg


 数据量为1万时插入、希尔、和冒泡算法的时间:


1669441278031.jpg


 数据量为10万时插入、希尔、和冒泡算法的时间:


1669441286294.jpg


数据量为1000万时算法的时间(只比较堆排序和希尔排序)


 1669441295415.jpg

相关文章
|
2天前
|
搜索推荐 C++
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(一)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
2天前
|
搜索推荐 索引
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(二)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
11天前
05_用一个栈实现另一个栈的排序
05_用一个栈实现另一个栈的排序
|
1天前
|
NoSQL 算法 Redis
Redis的实现三:c语言实现平衡二叉树,通过平衡二叉树实现排序集
本博客介绍了如何在C语言中实现一个平衡二叉树,并通过这个数据结构来模拟Redis中的排序集功能。
5 0
|
29天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
29天前
|
C语言
数据结构基础详解(C语言):图的基本概念_无向图_有向图_子图_生成树_生成森林_完全图
本文介绍了图的基本概念,包括图的定义、无向图与有向图、简单图与多重图等,并解释了顶点度、路径、连通性等相关术语。此外还讨论了子图、生成树、带权图及几种特殊形态的图,如完全图和树等。通过这些概念,读者可以更好地理解图论的基础知识。
|
2天前
|
人工智能 搜索推荐 算法
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(三)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
1月前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
1月前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。
|
1月前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。