数据结构(初阶)—— 排序算法(下)(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);
  }
}
目录
相关文章
|
29天前
|
算法
【初阶数据结构】复杂度算法题篇
该方法基于如下的事实:当我们将数组的元素向右移动 k 次后,尾部 kmodn 个元素会移动至数组头部,其余元素向后移动 kmodn 个位置。
|
30天前
|
机器学习/深度学习 人工智能 算法
【人工智能】线性回归模型:数据结构、算法详解与人工智能应用,附代码实现
线性回归是一种预测性建模技术,它研究的是因变量(目标)和自变量(特征)之间的关系。这种关系可以表示为一个线性方程,其中因变量是自变量的线性组合。
38 2
|
2月前
|
机器学习/深度学习 存储 算法
【数据结构】算法的复杂度
算法的时间复杂度和空间复杂度
50 1
【数据结构】算法的复杂度
|
29天前
|
算法
【初阶数据结构篇】二叉树算法题
二叉树是否对称,即左右子树是否对称.
|
29天前
|
算法 索引
【初阶数据结构篇】单链表算法题进阶
深拷贝应该正好由 n 个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。
|
29天前
|
存储 算法
【初阶数据结构篇】顺序表和链表算法题
此题可以先找到中间节点,然后把后半部分逆置,最近前后两部分一一比对,如果节点的值全部相同,则即为回文。
|
2月前
|
搜索推荐 算法
【数据结构】排序算法——Lesson2
【7月更文挑战第24天】
16 3
|
1月前
|
存储 缓存 算法
深入解析B树:数据结构、存储结构与算法优势
深入解析B树:数据结构、存储结构与算法优势
|
2月前
|
存储 算法 Python
“解锁Python高级数据结构新姿势:图的表示与遍历,让你的算法思维跃升新高度
【7月更文挑战第13天】Python中的图数据结构用于表示复杂关系,通过节点和边连接。常见的表示方法是邻接矩阵(适合稠密图)和邻接表(适合稀疏图)。图遍历包括DFS(深度优先搜索)和BFS(广度优先搜索):DFS深入探索分支,BFS逐层访问邻居。掌握这些技巧对优化算法和解决实际问题至关重要。**
26 1
|
2月前
|
算法 安全 调度
逆天改命!Python高级数据结构堆(Heap)与优先队列,让你的算法效率飙升至宇宙级!
【7月更文挑战第8天】Python的heapq模块和queue.PriorityQueue实现了堆和优先队列,提供高效算法解决方案。堆用于Dijkstra算法求解最短路径,例如在图论问题中;PriorityQueue则在多线程下载管理中确保高优先级任务优先执行。这两个数据结构提升效率,简化代码,是编程中的强大工具。
32 0