【交换排序】冒泡排序 与 快速排序

简介: 【交换排序】冒泡排序 与 快速排序

1.冒泡排序

假设升序。每次遍历,两两比较,将大的元素向后交换,直到选出最大的元素放在最后,这时已经确定了升序中最后一个元素,然后多次遍历前面无序的元素,每次可以确定一个最大的数,直到排序完成。


动态图解:

代码实现:

//交换函数
void Swap(int* p1, int* p2)
{
  int t = *p1;
  *p1 = *p2;
  *p2 = t;
}
void BubbleSort(int* arr, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    for (int j = 0; j < n - 1 - i; j++)
    {
      if (arr[j] > arr[j + 1])
      {
        Swap(&arr[j], &arr[j + 1]);
      }
    }
  }
}


如果在排序有序数据时,我们还可以优化一下,提高一下效率,代码如下

void BubbleSort(int* arr, int n)
{
  for (int i = 0; i < n - 1; i++)
  {
        int flag = 1;//假设已经有序
    for (int j = 0; j < n - 1 - i; j++)
    {
      if (arr[j] > arr[j + 1])
      {
        Swap(&arr[j], &arr[j + 1]);
                flag = 0;//发生交换,说明无序
      }
    }
        if(flag == 1)//已经有序,不在继续排序
        {
            break;
        }
  }
}


冒泡排序的特性总结:

  1. 冒泡排序是─种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定


2.快速排序

2.1 递归实现

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


图示:

代码实现:

假设按照升序对array数组中【left, right】左右都是闭区间中的元素进行排序。

void QuickSort(int* a, int left, int right)
{
  if (left >= right)
  {
    return;
  }
    //按照基准值(中间位置)对array数组的[left, right]区间中的元素进行划分
  int keyi = PartSort(a, left, right);
    //划分成功后以div为边界形成了左右两部分[left, keyi-1]和[key+1,right]
    //递归排 [left, keyi-1]
  QuickSort(a, left, keyi - 1);
    //递归排[key+1,right]
  QuickSort(a, keyi + 1, right);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,大家在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。


将区间按照基准值划分为左右两半部分(PartSort)的常见方式有:


1. hoare 版本


我们先看一下动图,方便理解

巧妙之处:

  1. 左边做key,右边先走;保障了相遇位置的值比key小,或者就是key的位置
  2. 右边做key,左边先走;保障了相遇位置的值比key大,或者就是key的位置


我们这里使用第一种方法:


L和R相遇无非就是两种情况,L遇R和R遇L:


  1. 情况一:L遇R,R是停下来,L在走,R先走,R停下来的位置一定比key小,相遇的位置就是R停下的位置,就一定比key要小
  2. 情况二:R遇L,在相遇这一轮,L就没动,R在移动,跟L相遇,相遇位置就是L的位置,L的位置就是key的位置或者这个位置交换过,换成了比key小的,相遇L位置一定比key小

代码实现:

int PartSort1(int* a, int left, int right)
{
  int keyi = left;
  while (left < right)
  {
    //右边找小 与key相等的数据放在左边右边都可以
    while (left < right && a[right] >= a[keyi])
    {
      right--;
    }
    //左边找大 与key相等的数据放在左边右边都可以
    while (left < right && a[left] <= a[keyi])
    {
      left++;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[keyi], &a[right]);
  return right;
}


2. 挖坑法

left 和 right 中有一个一定是坑位,右边找小,左边找大,找到就将值放入原坑位,该位置成为新坑位。

代码实现:

int PartSort2(int* a, int left, int right)
{
  int hole = left;
  int key = a[left];
  while (left < right)
  {
    //右边找小
    while (left < right && a[right] >= key)
    {
      right--;
    }
    a[hole] = a[right];
    hole = right;
    //左边找大
    while (left < right && a[left] <= key)
    {
      left++;
    }
    a[hole] = a[left];
    hole = left;
  }
  a[hole] = key;
  return hole;
}

3. 前后指针版本


  1. 最开始prev和cur相邻的
  2. 当cur遇到比key的大的值以后,他们之间的值都是比key大的值
  3. cur找小,找到小的以后,跟++prev位置的值交换,相当于把大翻滚式往右边推同时把小的换到左边


代码实现:

int PartSort3(int* a, int left, int right)
{
  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;
}


2.2 快速排序优化

当我们遇到有序数据时,由于我们的key是选的第一个元素,时间复杂度会变成O(N^2)。有两种优化方法:

  1. 三数取中法选key
  2. 随机数选key

我们这里讲一下第一种方法:让三个数作比较,取中间值

三数取中,代码实现:

int GetMidIndex(int* a, int left, int right)
{
  int mid = (left + right) / 2;
  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 right;
    }
    else
    {
      return left;
    }
  }
}


然后我们需要在上面3种划分方式开头都加上下面代码,就可以达到优化的目的

int midi = GetMidIndex(a, left, right);
Swap(&a[midi], &a[left]);


2.3 非递归实现

我们这里使用栈来实现,栈内存放需要划分的区间端点值,利用栈先入后出的特点模拟实现递归

void QuickSortNonR(int* a, int left, int right)
{
  Stack st;
  StackInit(&st);
  //入栈
  StackPush(&st, right);
  StackPush(&st, left);
  while (!StackEmpty(&st))
  {
    left = StackTop(&st);
    StackPop(&st);
    right = StackTop(&st);
    StackPop(&st);
    int keyi = PartSort1(a, left, right);
    //想先处理左边,就先右边区间先入栈
        //以基准值为分割点,形成左右两部分:[left, keyi-1]和[keyi+1,right)
    if(right > keyi + 1)
    {
      StackPush(&st, right);
      StackPush(&st, keyi + 1);
    }
    if (left < keyi - 1)
    {
      StackPush(&st, keyi - 1);
      StackPush(&st, left);
    }
  }
  StackDestroy(&st);
}

当然也可以使用队列来模拟。队列相当于广度优先,二叉树中的层序遍历,栈是深度优先,二叉树的先序遍历。

快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定


本篇结束!我们下一篇文章来学习排序第四课【归并排序】。

相关文章
|
4月前
|
存储 搜索推荐 算法
|
5月前
|
机器学习/深度学习 搜索推荐 算法
【C/排序算法】:快速排序和冒泡排序
【C/排序算法】:快速排序和冒泡排序
38 0
|
5月前
|
搜索推荐
冒泡排序、选择排序、二分查找
冒泡排序、选择排序、二分查找
19 0
|
存储 搜索推荐 算法
八大排序算法-直接插入排序、希尔排序、直接选择排序、冒泡排序、堆排序、快速排序、归并排序、基数排序(下)
八大排序算法-直接插入排序、希尔排序、直接选择排序、冒泡排序、堆排序、快速排序、归并排序、基数排序(下)
|
存储 搜索推荐 Java
【排序算法】冒泡排序、选择排序、插入排序
【排序算法】冒泡排序、选择排序、插入排序
108 0
【排序算法】冒泡排序、选择排序、插入排序
|
搜索推荐 算法
排序算法:冒泡排序,插入排序,选择排序,归并排序,快速排序
排序算法:冒泡排序,插入排序,选择排序,归并排序,快速排序
|
搜索推荐 算法
八大排序-直接插入排序、希尔排序、直接选择排序、冒泡排序、堆排序、快速排序、归并排序、基数排序(上)2
八大排序-直接插入排序、希尔排序、直接选择排序、冒泡排序、堆排序、快速排序、归并排序、基数排序(上)2
|
搜索推荐 算法 Shell
八大排序-直接插入排序、希尔排序、直接选择排序、冒泡排序、堆排序、快速排序、归并排序、基数排序(上)1
八大排序-直接插入排序、希尔排序、直接选择排序、冒泡排序、堆排序、快速排序、归并排序、基数排序(上)1
171 0
|
存储 人工智能 搜索推荐
【排序算法(三)】交换排序(冒泡排序&&快速排序)(上)
【排序算法(三)】交换排序(冒泡排序&&快速排序)(上)
|
搜索推荐 算法
【排序算法(三)】交换排序(冒泡排序&&快速排序)(下)
【排序算法(三)】交换排序(冒泡排序&&快速排序)(下)