【排序】插入排序,希尔排序,选择排序,冒泡排序,堆排序详解及稳定性分析

时间:2021-09-27 22:09:20

插入排序

直接插入排序

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

在这里要注意直接插入排序的前提,是插入到一个已经排好序的有序序列中,这表明,我们在插入一个数之前,必须保证被插入的序列是有序序列。

  所以我们在插入之前,要先构造一个有序序列, 所以从第一个参数开始,我们一个一个插入,并且都保证插入后序列为有序序列即可。
  所以插入顺序应如下图所示:
  【排序】插入排序,希尔排序,选择排序,冒泡排序,堆排序详解及稳定性分析
  那么应该怎么描述以上的过程呢?
  其实有很多中方法:

第一种,swap

for(int i = 1;i < n;i++)
for(int j = i;j > 0 && a[j-1] > a[j];j--)
swap(a[i],a[j]);

  但是,这种方法调用swap函数,速度较慢,我们可以使用临时变量来替代swap函数:
  

int tmp = a[j];
a[j] = a[j-1];
a[j-1] = tmp;

  上面的代码又会在内循环中不停地给变量tmp赋相同的值,所以将临时变量tmp定义提出来。
  最终优化过的完整代码如下:
  

void InsertSort(int* a, size_t n)
{
assert(a);

for (int i = 0; i < n-1; ++i)
{
int end = i;
int tmp = a[end+1];

while (end >= 0 && a[end] > tmp)
{
a[end+1] = a[end];
--end;
}
a[end+1] = tmp;
}
}

稳定性

  直接插入排序是稳定排序,即相同的数据元素在排序前后不会改变二者之间的相对顺序。

希尔排序

  希尔排序又叫缩小增量排序,其实希尔排序就是插入排序的一种,只不过对插入排序做了优化。
  在插入排序中,我们每次相当于每隔一个的再插入,希尔排序的思想是:

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

  过程为:
  

  • 首先定义一个增量值称为gap,我一般去gap为size的三分之一加一。
  • 以gap为间距分组并对每一组进行直接插入排序。
  • 在对gap进行除三加一操作
  • 重复以上,知道gap不大于0为止,即排序完成

    具体实现过程如下图:

【排序】插入排序,希尔排序,选择排序,冒泡排序,堆排序详解及稳定性分析
实现代码如下:

void ShellSort(int* a, size_t n)
{

int gap = n;
while (gap > 1) /
{
gap = gap/3+1;
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end+gap];
while (end >= 0 && a[end] > tmp)
{
a[end+gap] = a[end];
end -= gap;
}
a[end+gap] = tmp;
}
}
}

稳定性

  希尔排序是不稳定的排序,因为我们将原序列分组了,不能保证两个相同的数据在同一组,所以导致其是不稳定排序。

选择排序

  它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完,即变为有序序列。
  选择排序需要在进行排序前确定好排序后序列为升序序列还是降序序列,以判断将最大/最小元素放到序列起始位置还是最后的位置。
  实现代码为:
  

void SelectSort(int* a, int n)
{
assert(a);

for (int end = n-1; end > 0; --end)
{
int maxIndex = 0;
for (int i = 1; i <= end; ++i)
{
if (a[maxIndex] < a[i])
{
maxIndex = i;
}
}
swap(a[end],a[maxIndex]);
}
}

从代码中我们可以看到,每一次将最大的元素放入序列尾,每放入一个,end值减一,即将待排序序列范围缩小了一个。

选择排序的优化

  既然每一次我们都会遍历待排序序列,那何尝不同时将最大值,最小值都找出来,分别放入待排序序列的头和尾呢?
  改进后的方法,在每一次遍历时,不仅找出最大值,还找出最小值,在遍历完毕后,将最大值与序列尾值交换,将最小值与序列头值交换。
需要注意的一点是:

当我们交换了最小值和序列头元素后,如果最大值就在头元素那,一旦发生了最小值交换,那么最大值此时就不在头元素那了,因为上面已经被交换到最小值下标处了,所以交换最大值时,必须进行判断。分情况处理。

如下图所示:
【排序】插入排序,希尔排序,选择排序,冒泡排序,堆排序详解及稳定性分析

  代码如下:
  

void SelectSort_OP(int* a, int n)
{
assert(a);

//[]
int left = 0, right = n-1;
while (left < right)
{
int minIndex = left;
int maxIndex = right;
for (int i = left; i <= right; ++i)
{
if (a[i] < a[minIndex])
minIndex = i;

if (a[i] > a[maxIndex])
maxIndex = i;
}

swap(a[minIndex], a[left]);

// 修正maxIndex的位置
if (maxIndex == left)
maxIndex = minIndex;

swap(a[maxIndex], a[right]);

++left;
--right;
}
}

稳定性

   选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。

冒泡排序

  它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
  【排序】插入排序,希尔排序,选择排序,冒泡排序,堆排序详解及稳定性分析
  代码如下:
  

void BubbleSort(int* a, int n)
{
assert(a);

for (int end = n-1; end > 0; --end)
{
for (int i = 1; i <= end ; ++i)
{
if (a[i-1] > a[i])
{
swap(a[i-1], a[i]);
}
}
}
}

冒泡排序的优化

  我们已经知道当走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成,所以我们可以设置一个标志位来判断是否已经排序完毕。

void BubbleSort(int* a, int n)
{
assert(a);

for (int end = n-1; end > 0; --end)
{
bool exchange = false;
for (int i = 1; i <= end ; ++i)
{
if (a[i-1] > a[i])
{
swap(a[i-1], a[i]);
exchange = true;
}
}

if (exchange == false)
break;
}
}

稳定性

  冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

堆排序

  我们都知道堆这个数据结构,是用数组模拟树的形式进行的排序。但是堆数据结构只保证父节点的值大于或者小于子节点的值,并不保证整体的顺序。但是我们可以利用根节点的值永远是最大,或者最小的这个特性,利用根节点找出最值,再与尾位置互换,即可排序成功。
  有关堆数据结构的具体知识再次我就不赘述了。
  首先先上堆结构的向下调整法代码:
  

void AdjustDown(int* a, int root, int n)
{
int parent = root;
int child = parent*2+1;

while (child < n)
{
// 选孩子里面大的一个
if (child+1 < n && a[child+1] > a[child])
{
++child;
}

if (a[parent] < a[child])
{
swap(a[parent], a[child]);
parent = child;
child = parent*2+1;
}
else
{
break;
}
}
}

然后进行排序

  • 首先我们要建堆
  • 然后每次找出[0,n]范围内的最值,即对0到n以根节点即0为root向下调整,找出最值
  • 再将最值与尾值互换
  • 尾值减一
  • 直到尾值到0为止即为有序序列

具体代码如下:


void HeapSort(int* a, int n)
{
assert(a);

//建大堆
for(int i = (n-2)/2; i >=0; --i)
{
AdjustDown(a, i, n);
}

// 选数据进行排序
int end = n-1;
while (end > 0)
{
swap(a[0], a[end]);
AdjustDown(a, 0, end);

--end;
}
}

稳定性

  因为每次根节点的值都是不确定的,所以我们也不能保证相同元素的父节点是相同的,所以堆排序是不稳定的