数据结构(初阶)—— 排序算法(下)(1)

简介: 数据结构(初阶)—— 排序算法(下)(1)

一、冒泡排序

       基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1.动图演示

2.代码实现

// 冒泡排序
void Swap(int* px, int* py)
{
  int tmp = *px;
  *px = *py;
  *py = tmp;
}
void BubbleSort(int* a, int n)
{
  int end = n;
  while (end > 0)
  {
    int exchange = 0;
    for (int j = 0; j < end - 1; j++)
    {
      if (a[j] > a[j + 1])
      {
        exchange = 1;
        Swap(&a[j], &a[j + 1]);
      }
    }
    --end;
    if (exchange == 0)
    {
      break;
    }
  }
  /*for (int i = 0; i < n - 1; i++)
  {
    for (int j = 0; j < n - i - 1; j++)
    {
      if (a[j] > a[j + 1])
      {
        Swap(&a[j], &a[j + 1]);
      }
    }
  }*/
}

二、快速排序

       快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

基本思想如下:

1ecd1b2606ed46e9956a89f231c9802c.png

1. 递归实现

1.Hoare版本(含动图演示)

动图演示

image.gif

代码实现

int Partion1(int* a,int left,int right)
{
  int keyi = left;
  while (left < right)
  {
    //右边先走,找小
    while (a[right] > a[keyi])
    {
      --right;
    }
    //左边再走,找大
    while (a[left] < a[keyi])
    {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[left], &a[keyi]);
  return left;
}

上述代码存在两大缺陷:

场景1:

如果是一个有序的数组(升序),每个数都比第一个数大,小黄就没人管了,撒开腿就一直跑;

1ecd1b2606ed46e9956a89f231c9802c.png

场景2:

如果这个数组元素都相同,小黄先跑,发现跑不动;(死循环)

image.png

针对刚才的代码很明显在这两种场景下一定会出问题;

如何解决:

1.确保 右边找小左边找大 的时候,判断它(小黄或小白)的下标有没有越界;

2.要考虑元素相等的情况;

int Partion1(int* a,int left,int right)
{
  int keyi = left;
  while (left < right)
  {
    //右边先走,找小
    while (left < right && a[right] >= a[keyi])
    {
      --right;
    }
    //左边再走,找大
    while (left < right && a[left] <= a[keyi])
    {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[left], &a[keyi]);
  return left;
}

 单趟排序完成之后,比key大的数都放在右边,比key小的数都放在左边,只要以key为基准值,它的左子区间再有序,右子区间再有序,那么整体就有序了;这种问题和二叉树很类似;第一趟排序找了一个key,分成了左右区间,只要为左右区间各找一个key,不断的去划分,那么久把整个数组排序完成了;

int Partion1(int* a,int left,int right)
{
  int keyi = left;
  while (left < right)
  {
    //右边先走,找小
    while (left < right && a[right] >= a[keyi])
    {
      --right;
    }
    //左边再走,找大
    while (left < right && a[left] <= a[keyi])
    {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[left], &a[keyi]);
  return left;
}
void QuickSort(int* a, int left, int right)
{
  if (left >= right)
    return;
    int keyi = Partion1(a, left, right);/*hoare版本*/
        //区间划分[begin , keyi-1] keyi [keyi+1 , end]
    QuickSort(a, left, keyi - 1);//左子区间
    QuickSort(a, keyi + 1, right);//右子区间
  }
}

如何定位基准值(key):

       先看下面的图例,发现快排在针对无序的数组进行排序时,它的效率是很快的,但是在有序或接近有序的情况下,就会变得很糟糕;这里最关键的就是key的选择问题; 也就是如何针对快排对于数组有序情况下选key的问题;

1ecd1b2606ed46e9956a89f231c9802c.png

为了解决选key的问题,可以采用三数取中法;

三数取中法:最左边的数、中间数、最右边的数;

1ecd1b2606ed46e9956a89f231c9802c.png

对这三个数进行比较,选出一个中间值(既不是最大也不是最小),作为key; 那么针对于有序的情况,就会瞬间变得很好,直接将数据二分;

快排完整动图

image.gif

代码实现  

/*hoare版本*/
//快排(递归)
//三数取中
int GetMidIndex(int* a, int left, int right)
{
  //int mid = (left + right) / 2;
  int mid = left + ((right - left) >> 1);
  if (a[left] < a[mid]) 
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else//a[left] > a[mid]
  {
    if (a[mid] > a[right])
    {
      return mid;
    }
    else if (a[left] < a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
}
int Partion1(int* a,int left,int right)
{
  //三数取中
  int mini = GetMidIndex(a, left, right);
  Swap(&a[mini], &a[left]);
  int keyi = left;
  while (left < right)
  {
    //右边先走,找小
    while (left < right && a[right] >= a[keyi])
    {
      --right;
    }
    //左边再走,找大
    while (left < right && a[left] <= a[keyi])
    {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[left], &a[keyi]);
  return left;
}
//O(N*logN)
void QuickSort(int* a, int left, int right)
{
  if (left >= right)
    return;
  //小区间优化,当分割到小区间时,不在用递归思路让这段子区间有序
  //对于递归快排,减少递归次数
  if (right - left + 1 < 10)
  {
    InsertSort(a + left, right - left + 1);
  }
  else
  {
    int keyi = Partion1(a, left, right);/*hoare版本*/
    //区间划分[begin , keyi-1] keyi [keyi+1 , end]
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
  }
}

在上面的代码中有这样一段代码:

    if (right - left + 1 < 10)
  {
    InsertSort(a + left, right - left + 1);
  }
  else
  {
    int keyi = Partion1(a, left, right);/*hoare版本*/
    //区间划分[begin , keyi-1] keyi [keyi+1 , end]
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
  }

由于采用递归的方式进行快排,如果数据量很大,就会造成栈的溢出;为了解决这个问题,采用了一个小区间优化的方法,在不断划分区间去进行递归的时候,越往下划分区间越来越小,但是区间数量越来越多,递归次数也越来越多;我们采取当区间相差小于10(最低给10)时,直接让这些区间的数据进行直接插入排序;

1ecd1b2606ed46e9956a89f231c9802c.png

2.挖坑法 (含动图演示)

1.基本思路:

       先将第一个变量存储在临时变量key中,右边先走,去找小扔到左边的坑,自己成为新的坑;然后左边再走,去找大扔到右边的坑( pivot ),自己成为新的坑( pivot );直到两者相遇把保存的key的值放进坑( pivot );


image.png

2.动图演示

下图为挖坑法的单趟演示,其完整排序和上面的快排完整版类似,需要划分左右子区间; 进行递归调用

20201204182323419.gif

3.代码实现

//挖坑法
//三数取中
int GetMidIndex(int* a, int left, int right)
{
  //int mid = (left + right) / 2;
  int mid = left + ((right - left) >> 1);
  if (a[left] < a[mid]) 
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else//a[left] >= a[mid]
  {
    if (a[mid] > a[right])
    {
      return mid;
    }
    else if (a[left] < a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
}
int Partion2(int* a, int left, int right)
{
  //三数取中
  int mini = GetMidIndex(a, left, right);
  Swap(&a[mini], &a[left]);
  int key = a[left];
  int pivot = left;
  while (left < right)
  {
    //右边找小,扔到左边的坑
    while (left < right && a[right] >= key)
    {
      --right;
    }
    a[pivot] = a[right];
    pivot = right;//自己形成新的坑
    //左边找大,扔到右边的坑
    while (left < right && a[left] <= key)
    {
      ++left;
    }
    a[pivot] = a[left];
    pivot = left;//自己形成新的坑
  }
  a[pivot] = key;
  return pivot;
}
void QuickSort(int* a, int left, int right)
{
  if (left >= right)
    return;
  //小区间优化,当分割到小区间时,不在用递归思路让这段子区间有序
  //对于递归快排,减少递归次数
  if (right - left + 1 < 10)
  {
    InsertSort(a + left, right - left + 1);
  }
  else
  {
    //int keyi = Partion2(a, left, right);//挖坑法
    //区间划分[begin , keyi-1] keyi [keyi+1 , end]
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
  }
}

3.前后指针法 (含动图演示)

1.基本思路:

       前后指针法的基本思想其实和前面两种方法类似,cur去找小,prev去找大;也会得到key的左边是小值右边是大值;

20210505110855951.gif

2.代码实现

//前后指针法
//三数取中
int GetMidIndex(int* a, int left, int right)
{
  //int mid = (left + right) / 2;
  int mid = left + ((right - left) >> 1);
  if (a[left] < a[mid]) 
  {
    if (a[mid] < a[right])
    {
      return mid;
    }
    else if (a[left] > a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
  else//a[left] >= a[mid]
  {
    if (a[mid] > a[right])
    {
      return mid;
    }
    else if (a[left] < a[right])
    {
      return left;
    }
    else
    {
      return right;
    }
  }
}
int Partion3(int* a, int left, int right)
{
  //三数取中
  int mini = GetMidIndex(a, left, right);
  Swap(&a[mini], &a[left]);
  int keyi = left;
  int prev = left;
  int cur = left +1;
  while (cur <= right)
  {
    if (a[cur] < a[keyi] && ++prev != cur)
    {
      Swap(&a[cur], &a[prev]);
    }
    cur++;
  }
  Swap(&a[prev], &a[keyi]);
  return prev;
}
void QuickSort(int* a, int left, int right)
{
  if (left >= right)
    return;
  //小区间优化,当分割到小区间时,不在用递归思路让这段子区间有序
  //对于递归快排,减少递归次数
  if (right - left + 1 < 10)
  {
    InsertSort(a + left, right - left + 1);
  }
  else
  {
    int keyi = Partion3(a, left, right);//前后指针法
    //区间划分[begin , keyi-1] keyi [keyi+1 , end]
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
  }
}
目录
相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
49 1
|
3月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
99 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
112 23
|
2月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
62 20
|
2月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
58 0
|
3月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
50 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
2月前
|
算法 vr&ar 计算机视觉
数据结构之洪水填充算法(DFS)
洪水填充算法是一种基于深度优先搜索(DFS)的图像处理技术,主要用于区域填充和图像分割。通过递归或栈的方式探索图像中的连通区域并进行颜色替换。本文介绍了算法的基本原理、数据结构设计(如链表和栈)、核心代码实现及应用实例,展示了算法在图像编辑等领域的高效性和灵活性。同时,文中也讨论了算法的优缺点,如实现简单但可能存在堆栈溢出的风险等。
59 0
|
3月前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
55 4