数据结构排序——详解快排及其优化和冒泡排序(c语言实现、附有图片与动图示意)

简介: 数据结构排序——详解快排及其优化和冒泡排序(c语言实现、附有图片与动图示意)

上次讲了选择排序和堆排序

今天就来快排和冒泡


1.快排


1.1基本介绍


快速排序(Quick Sort)是一种常用的排序算法,它是由英国计算机科学家Tony Hoare于1959年发明的。快速排序的基本思想是通过分治的策略将一个数组分成两个子数组,然后分别对这两个子数组进行排序。具体步骤如下:


选择一个基准元素(通常是数组的第一个元素,右边先行)。

将数组分割成两部分,使得左边的元素都小于等于基准元素,右边的元素都大于基准元素。这个过程叫做分区(Partition)

对分割后的两个子数组分别重复步骤1和2(利用递归),直到子数组的大小为1或0,此时数组已经有序

==优化:==如果本身就很接近有序,那效率就慢了(一个逆序变升序,keyi就一直在左边,递归也只有右侧,所以选择三个数来找中间大小,能让keyi尽量向数组中间靠近),所以设计了Getmid函数来取中间大小的数


1.2不同的分区方法及代码实现


1.2.1Hoare版


使用两个索引,一个从数组的左边开始向右移动,另一个从数组的右边开始向左移动,直到它们相遇。在这个过程中,如果左指针指向的元素大于基准元素且右指针指向的元素小于基准元素,则交换这两个元素。当两个指针相遇时,将基准元素(keyi指向的)与相遇位置的元素交换,这样基准元素就归位了

void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
int GetMid(int* a,int left, int right)//找中间的
{
  // a[left]      a[mid]           a[right]
  int mid = (left + right) / 2;
  if (a[left] < a[mid])
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])  // mid是最大值
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else // a[left] > a[mid]
  {
    if (a[left] < a[right])
    {
      return left;
    }
    else if (a[mid] < a[right])
    {
      return right;
    }
    else
    {
      return mid;
    }
  }
}
//一次排序
int OneSort1(int* a, int left, int right)//使keyi位置的元素处于正确的位置上
{
  int mid = GetMid(a, left, right);
  Swap(&a[mid], &a[left]);//现在left处是三者的中间值了
  //左边第一个为key,右边先走才能保证相遇处比啊a[keyi]小
  int keyi = left;
  while (left < right)
  {
    while (a[right] >= a[keyi] && left < right)//右边先走
    {
      right--;
    }
    while (a[left] <= a[keyi] && left < right)//左侧找大的
    {
      left++;
    }
    Swap(&a[left], &a[right]);//找到一个大和一个小的就交换
  }
  Swap(&a[keyi], &a[left]);//把keyi放相遇位置
  return left;//返回相遇的索引
}
void QuickSort(int* a, int begin, int end)//升序
{
  if (begin >= end)
  {
    return;
  }
  // [begin, keyi-1] keyi [keyi+1, end]
  int keyi = OneSort1(a, begin, end);//找到keyi索引,才能分左右
  QuickSort(a, begin, keyi - 1);//左侧
  QuickSort(a, keyi + 1, end);//右侧
}
int main()
{
  int a[] = { 6,1,2,7,9,3,4,5,10,8 };
  printf("排序前:");
  for (int i = 0; i < sizeof(a) / sizeof(int); i++)
  {
    printf("%d ", a[i]);
  }
  printf("\n");
  QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
  printf("排序后:");
  for (int i = 0; i < sizeof(a) / sizeof(int); i++)
  {
    printf("%d ", a[i]);
  }
  printf("\n");
  return 0;
}


1.2.2挖坑版


选择基准元素后,先将基准元素保存到临时变量中,然后使用左右索引的方式找到需要交换的元素,将这些元素填入基准元素的位置,最后将基准元素填入最后一个空出来的位置

int OneSort2(int* a, int left, int right)//挖坑
{
  int mid = GetMid(a, left, right);
  Swap(&a[mid], &a[left]);//现在left处是三者的中间值了
  int key = a[left];//保存基准元素
  int hole = left;//储存坑下标,不能直接赋值为0
  while (left < right)
  {
    while (a[right] >= key && left < right)//右边先走,没有等号两侧出现相同值会死循环
    {
      right--;
    }//找到了就去赋值到第一个“坑”
    a[hole] = a[right];
    hole = right;//更新坑
    while (a[left] <= key && left < right)//左侧找大的
    {
      left++;
    }
    a[hole] = a[left];
    hole = left;
  }
  Swap(&key, &a[left]);//把keyi放相遇位置
  return left;//返回相遇的索引
}


1.2.3 前后指针版


pre指向第一个,cur指向下一个。cur找小后,pre++,然后交换(做到大的向后推),最后cur出数组结束


prev的情况有两种:


在cur还没遇到比key大的值时候,prev紧跟着cur

在cur还遇到比key大的值时候,prev在比key大的一组值的前面

本质是把一段大于key的区间,往后推,同时小的甩到左边去

int OneSort3(int* a, int left, int right)//挖坑
{
  int mid = GetMid(a, left, right);
  Swap(&a[mid], &a[left]);
  int keyi = left;
  int pre = left;
  int cur = left + 1;
  while (cur <= right)
  {
    if (a[cur] < a[keyi])
    {
      pre++;
      Swap(&a[cur], &a[pre]);
    }
    cur++;
  }
  Swap(&a[pre], &a[keyi]);
  return pre;
}


1.3快排的优化


1.3.1三数取中选key


从待排序数组的首、尾和中间位置各选取一个元素,然后取它们的中间值作为基准元素,确保选择的基准元素相对中间位置的元素更为接近


代码在Hoare版已经展示过了

int GetMid(int* a,int left, int right)//找中间的
{
  // a[left]      a[mid]           a[right]
  int mid = (left + right) / 2;
  if (a[left] < a[mid])
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])  // mid是最大值
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else // a[left] > a[mid]
  {
    if (a[left] < a[right])
    {
      return left;
    }
    else if (a[mid] < a[right])
    {
      return right;
    }
    else
    {
      return mid;
    }
  }
}


1.3.2递归到小的子区间时,可以考虑使用插入排序


当递归到小的子区间时,可以考虑使用插入排序来优化快速排序。因为插入排序在小规模数据上的排序效率通常比快速排序更高==(当数量比较少时,也没必要在递归好几层了)==

void InsertSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    int end = i;
    int tmp = a[end + 1];
    while (end >= 0)
    {
      if (a[end] > tmp)
      {
        a[end + 1] = a[end];
      }
      else
      {
        break;
      }
      end--;
    }
    a[end + 1] = tmp;
  }
}
int OneSort3(int* a, int left, int right)//挖坑
{
  int mid = GetMid(a, left, right);
  Swap(&a[mid], &a[left]);
  int keyi = left;
  int pre = left;
  int cur = left + 1;
  while (cur <= right)
  {
    if (a[cur] < a[keyi])
    {
      pre++;
      Swap(&a[cur], &a[pre]);
    }
    cur++;
  }
  Swap(&a[pre], &a[keyi]);
  return pre;
}
void QuickSort(int* a, int begin, int end)//升序
{
  if (begin >= end)
  {
    return;
  }
  if ((end - begin + 1) > 10)
  {
    // [begin, keyi-1] keyi [keyi+1, end]
    int keyi = OneSort3(a, begin, end);
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
  else
  {
    //用插入排序
    InsertSort(a + begin, end - begin + 1);
  }
}


1.3.3大量重复数据采用三路划分


快速排序的三路划分通过将数组分为小于、等于=和大于基准元素的三个部分==,有效地处理了重复元素,提高了算法的效率


快速排序的三路划分算法的基本思想是使用三个指针/下标来维护三个区域:小于基准元素的区域、等于基准元素的区域和大于基准元素的区域。在每一次遍历数组的过程中,将数组分为这三个区域,并将指针移动到合适的位置。最终,数组会被划分成小于基准元素、等于基准元素和大于基准元素的三个部分

基本步骤:

void QuickSort(int* a, int left, int right)
{
  if (left >= right)
  {
    return;
  }
  int begin = left;
  int end = right;
  int mid = GetMid(a, left, right);
  Swap(&a[mid], &a[left]);
  int cur = left + 1;
  int key = a[left];//储存一下,后面比较来用,用a[left]会被替代
  while (cur <= right)
  {
    if (a[cur] < key)
    {
      Swap(&a[cur], &a[left]);
      cur++;
      left++;
    }
    else if (a[cur] == key)
    {
      cur++;
    }
    else
    {
      Swap(&a[cur], &a[right]);
      right--;
    }
  }
  QuickSort(a, begin, left - 1);
  QuickSort(a, right + 1, end);
}


1.4快排非递归


快速排序的非递归实现通常使用栈来模拟递归调用的过程。在快速排序的递归实现中,每次递归调用都将函数参数压入栈中,然后在递归返回时再从栈中弹出参数(二者性质相同)。


非递归实现则需要手动维护一个栈,将需要处理的子序列的起始和结束位置压入栈中,然后循环处理栈中的元素,直到栈为空(递归一次就用一次)

void QuickSortNonR(int* a, int begin, int end)//利用栈,先想好先排左侧再排右侧
{
  ST st;
  STInit(&st);
  STPush(&st,end);//把各个区间的两侧的索引(整形)插入进Stack中
  STPush(&st,begin);//栈(后进先出),先排左侧所以后入左
  while (!STEmpty(&st))
  {
    int left = STTop(&st);
    STPop(&st);
    int right = STTop(&st);
    STPop(&st);
    int keyi = OneSort1(a, left, right);//得到基准元素下标
    // [begin, keyi-1] keyi [keyi+1, end]
    if (keyi + 1 < right)//等于说明就一个,没必要
    {
      STPush(&st, right);
      STPush(&st, keyi);
    }
    if (left < keyi-1)
    {
      STPush(&st, keyi-1);
      STPush(&st, left);
    }
  }
  STDestroy(&st);
}



2.冒泡排序


它重复地遍历要排序的列表,比较每对相邻的元素,并按照大小顺序交换它们。重复这个过程直到整个列表排序完成


步骤:


从列表的第一个元素开始,依次比较相邻的两个元素,如果它们的顺序不正确就交换它们。

继续遍历列表,重复上述比较和交换的过程,直到没有任何一对相邻元素需要交换为止。

列表排序完


void BobbleSort(int* a, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    for (int j = 0; j < n - 1 - i; j++)
    {
      if (a[j] > a[j + 1])
      {
        Swap(&a[j], &a[j + 1]);
      }
    }
  }
}

好啦,这次内容就到这里啦,下次带来归并排序,感谢大家支持!!!

目录
相关文章
|
27天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
45 1
|
1月前
|
存储 算法 搜索推荐
【趣学C语言和数据结构100例】91-95
本文涵盖多个经典算法问题的C语言实现,包括堆排序、归并排序、从长整型变量中提取偶数位数、工人信息排序及无向图是否为树的判断。通过这些问题,读者可以深入了解排序算法、数据处理方法和图论基础知识,提升编程能力和算法理解。
49 4
|
1月前
|
存储 机器学习/深度学习 搜索推荐
【趣学C语言和数据结构100例】86-90
本文介绍并用C语言实现了五种经典排序算法:直接插入排序、折半插入排序、冒泡排序、快速排序和简单选择排序。每种算法都有其特点和适用场景,如直接插入排序适合小规模或基本有序的数据,快速排序则适用于大规模数据集,具有较高的效率。通过学习这些算法,读者可以加深对数据结构和算法设计的理解,提升解决实际问题的能力。
45 4
|
1月前
|
存储 算法 数据处理
【趣学C语言和数据结构100例】81-85
本文介绍了五个经典算法问题及其C语言实现,涵盖图论与树结构的基础知识。包括使用BFS求解单源最短路径、统计有向图中入度或出度为0的点数、统计无向无权图各顶点的度、折半查找及二叉排序树的查找。这些算法不仅理论意义重大,且在实际应用中极为广泛,有助于提升编程能力和数据结构理解。
44 4
|
1月前
|
算法 数据可视化 数据建模
【趣学C语言和数据结构100例】76-80
本文介绍了五种图论算法的C语言实现,涵盖二叉树的层次遍历及广度优先搜索(BFS)和深度优先搜索(DFS)的邻接表与邻接矩阵实现。层次遍历使用队列按层访问二叉树节点;BFS利用队列从源节点逐层遍历图节点,适用于最短路径等问题;DFS通过递归或栈深入图的分支,适合拓扑排序等场景。这些算法是数据结构和算法学习的基础,对提升编程能力和解决实际问题至关重要。
52 4
|
1月前
|
存储 算法 vr&ar
【趣学C语言和数据结构100例】71-75
本文介绍了五个C语言数据结构问题及其实现,涵盖链表与二叉树操作,包括按奇偶分解链表、交换二叉树左右子树、查找节点的双亲节点、计算二叉树深度及求最大关键值。通过递归和遍历等方法,解决了理论与实际应用中的常见问题,有助于提升编程能力和数据结构理解。
43 4
|
1月前
|
存储 算法 C语言
【趣学C语言和数据结构100例】66-70
本书《趣学C语言和数据结构100例》精选了5个典型的数据结构问题及C语言实现,涵盖链表与数组操作,如有序集合的集合运算、有序序列表的合并、数组中两顺序表位置互换、三递增序列公共元素查找及奇偶数重排。通过详细解析与代码示例,帮助读者深入理解数据结构与算法设计的核心思想,提升编程技能。
36 4
|
29天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
55 5
|
27天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
61 1
|
25天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
50 10