0
点赞
收藏
分享

微信扫一扫

算法基础(一):常见排序算法

笙烛 2022-01-31 阅读 80
算法

记忆口诀: 选泡插,快归堆兮桶计基。恩方恩老恩要散,对恩加K恩乘K。不稳稳稳不稳稳,不稳不稳稳稳稳

怎么调试bug?:  通读程序 -> 输出中间值 -> 剪功能 。 核心是定位

选择排序:

选择排序不稳定是因为有横跨交换。 最符合人类思维的排序方式,从第一个位置开始到最后一个位置,每个位置都要和后面的数比较一次,如果后面有更小的,就拿过来。

//选择排序
for (int i = 0; i < array.Length - 1; i++)
{
for (int j = i + 1; j < array.Length; j++)
{
if (array[j] < array[i])
{
int temp = array[j];
array[j] = array[i];
array[i] = temp;
}
}
}

冒泡排序:

与选择排序不同的是,首先,比较对象是“每每相邻”的元素;其次,最优的时间复杂度为O(n).

 //冒泡排序 
bool swapHappend = false;
for (int i = array.Length - 1; i > 0; i--)
{
swapHappend = false;//每次冒泡开始前都需要将默认值重置。
for (int j = 0; j < i; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
swapHappend = true;
}
}
if (!swapHappend) //当一次都没交换过,证明是已经排好的
{ //此时时间复杂度可以达到最优:O(n)
return;
}
}

插入排序:

对于基本有序的数组最好用,稳定。性能优于冒泡排序。插入排序的数组在排序过程中,一段是有序的(如: 1, 2, 3, 4, 5, 9, 7, 11, 56,  42, 33),无序端头的元素的跟有序端进行比较,然后寻找出这个无序元素的在有序端的位置然后插入(上述数组就是元素7插入到5与9之间),插入排序算法的优势成因就是:找到第一个可插入的位置之后便可以停止循环。

 //插入排序
for (int i = 1; i < array.Length; i++) //从第二个位置开始往前遍历
{
for (int j = i; j > 0; j--)
{
//由于数组前面部分是有序的,所以遇到第一个不可插入的位置
//就可以停止比较了
if (array[j - 1] > array[j])
{
int temp = array[j - 1];
array[j - 1] = array[j];
array[j] = temp;
}
else
{
break;
}
}
}

希尔排序:

插入排序的改进版,利用分组思想。相对于插入排序,首先,需要注意的是多了一个循环,该循环用于生成间隔数组;其次,应该注意分组的实现主要是依靠for循环的条件设置,边界编写容易出错。

 //Shell排序
//计算 Knuth 序列作为间隔
int h = 1;
while (h < array.Length / 3)
{
h = 3 * h + 1;
}
//开始Shell排序
for (int gap = h; gap > 0; gap = (gap - 1) / 3)
{
for (int i = gap; i < array.Length; i++)
{
for (int j = i; j > gap - 1; j -= gap)
{
if (array[j] < array[j - gap])
{
int tempNum = array[j - gap];
array[j - gap] = array[j];
array[j] = tempNum;
}
else
{
break;
}
}
}
}

归并排序:

使用了递归,递归都需要考虑一个bottom,即递归调用到最后一层的情况。

 //归并排序函数
static void mergeSort(int[] array, int aHead, int aTail)
{
/*参数说明
arayOutput:原数组 aHead:数组头指针
aTail:数组尾指针 aMiddle:分割为两个数组的标记指针
*/

if (aHead == aTail) { return; } //只有一个元素时,则返回

int i = (aHead + aTail) / 2; //原数组遍历指针
int j = i + 1; //原数组遍历指针
int[] arayOutputCopy = new int[aTail - aHead + 1]; //工作空间
int k = 0; //工作空间遍历指针

mergeSort(array, aHead, i);
mergeSort(array, j, aTail);

if (array[i] <= array[j]) { return; } //当数组有序,则返回

//选择较小的元素放入工作空间
while (aHead <= i aTail)
{
if (array[aHead] <= array[j])
{
arayOutputCopy[k++] = array[aHead++];
}
else
{
arayOutputCopy[k++] = array[j++];
}
}

//未遍历到的数据进行一次遍历
while (aHead <= i)
{ arayOutputCopy[k++] = array[aHead++]; }
while (j <= aTail)
{ arayOutputCopy[k++] = array[j++]; }

//最后结果赋值给原数组
for (int l = arayOutputCopy.Length - 1; l >= 0; l--, aTail--)
{ array[aTail] = arayOutputCopy[l]; }

}

快速排序:

容易出bug ,需要验证极端情况,包括用于判定比较的数在数组的边界、重复数、只有两个数的情况。对于快排的理解,参考博客简单快速排序_闲来之笔-CSDN博客_简单快速排序 ,这篇博客中 “投石 -> 确定脏数据 ->移动”对于记忆该算法有帮助,这里的快速排序我第一次见是在严蔚敏版c语言数据结构那本教材中。除了“投石法”实现单轴快排,也有其他的实现方式。


static void QuickSortIterative(int[] array, int Low, int High)
{
if (Low < High)
{
int pivot = QuickSort(array, Low, High);
QuickSortIterative(array, Low, pivot-1);
QuickSortIterative(array, pivot+1, High);
}
}

static int QuickSort(int[] array, int Low, int High)
{
int pivot = array[Low];

while (Low < High)
{
while (Low < High && array[High] >= pivot )
{ --High; }//循环结束说明在左侧(HIGH)找到了右侧数,或者遍历完毕
array[Low] = array[High];

while (Low < High && array[Low] <= pivot)
{ ++Low; } //循环结束说明在右侧(LOW)找到了左侧数,或者遍历完毕
array[High] = array[Low];
}

array[High] = pivot;

return Low;
}

双轴快速排序(DualPivotSort):

该算法是荷兰国旗问题的解法 ,双轴快排中有三个指针,其中两个指针为慢指针,用于标记两个轴(pivot)的位置,将数组分为了三个区域;一个快指针,快指针则是用于遍历数组,每次遍历一个元素,确定应该在在三个区域中的哪一块,不容的区域执行代码不同,受快指针的移动方向影响。


public static void DualPivodSortIterative(int[] array, int low, int high)
{
if (low < high) {
int[] pos = DualPivodSort(array, low, high);
DualPivodSortIterative(array, low, pos[0]-1);
DualPivodSortIterative(array, pos[0] + 1, pos[1] - 1);
DualPivodSortIterative(array, pos[1] + 1, high);
}
}
public static int[] DualPivodSort(int[] array, int low, int high)
{
//使 第一个轴 <= 第二个轴
if (array[low] > array[high]) { swapArray(array, low, high); }

//记录轴的位置
int pivot1 = low;
int pivot2 = high;

//工作指针 middle
int middle = low + 1;

//有三种情况,因此if有三个分支,遍历方向为 low -> high ,因此第二if不需要 middle++
while (middle < high)
{
if (array[middle] < array[pivot1]) {
swapArray(array, ++low, middle++);
}
else if (array[middle] > array[pivot2]) {
swapArray(array,--high,middle );
}
else { middle++; }
}

//遍历完后,让轴回到轴位,使轴左边小于轴,轴右边大于轴
swapArray(array,pivot1,low);
swapArray(array,pivot2,high);

int[] pos = { low, high };
return pos;
}

 双轴快排序的改进:

双轴快速排序_闲来之笔-CSDN博客_双轴快排  ,对原本的三种情况进行了判定改进。

......
while (middle < high)
{
if (array[middle] < array[pivot1])
{
swapArray(array, ++low, middle);
}
else if (array[middle] > array[pivot2])
{
high--;
if (array[high] < array[low])
{
low++;
swapArray(array, high, middle);
swapArray(array, middle, low);
while (low + 1 < high && array[low + 1] < array[pivot1]) {
middle = ++low;
}
}
else if (array[pivot1] <= array[high] && array[high] <= array[pivot2]) {
swapArray(array, middle, high);
}
while (high - 1 >= middle && array[high - 1] > array[pivot2]) {
high--;
}
}
middle++;
}
......

堆排序:

堆排序需要前置知识储备:“完全二叉树的数组存储方式”、“由完全二叉树总结点数 得出 非叶子结点数 、叶子结点数”

        //排序函数,先构建大顶堆,然后循环“摘顶->构建新堆”过程
static void HeapSort(int[] array)
{
//大顶堆构建,从底层的最后一个非叶结点向根节点遍历
//遍历顺序以及起始点不能变化,否则需要重写 HeapConstruct 函数
for (int i = array.Length / 2 - 1; i > -1; i--)
{
HeapConstruct(array, i, array.Length);
}
//进行排序,即“摘顶过程”,每取一次根节点(结点最大值)与最后一个位置的结点
//进行交换后,我们便确定了一个结点的排序位置,树的结点总数就会减1
//摘顶完成后,树节点总数减少了,大顶堆也受到破坏,因此需要从根节点开始往后
//遍历重新使二叉树变成大顶堆
for (int j = array.Length - 1; j > 0; j--)
{
swapArray(array, j, 0);
HeapConstruct(array, 0, j);
}

}

//构建大顶堆,输入参数分别为:存储二叉树的数组、起始节点、节点总数
static void HeapConstruct(int[] array, int node,int nodeNum)
{
int pointer = node; //遍历指针

//当前指针结点为"非叶子结点"则执行循环
while (pointer < nodeNum / 2)
{
int tempPointer = pointer; //当前指针备份
int sonL = pointer * 2 + 1; //当前指针结点左孩子(完全二叉树的非叶结点,左孩子必然存在)
int sonR = pointer * 2 + 2; //当前指针结点右孩子(不一定存在)

//右孩子存在,且为较大数值
if (sonR < nodeNum && array[sonL] < array[sonR]){
pointer = sonR;
}else { //只存在左孩子,或者左孩子为较大数值
pointer = sonL;
}

//子结点的数值更大则交换,否则终止遍历
if (array[tempPointer] < array[pointer]) {
swapArray(array, tempPointer, pointer);
}else {
break;
}
}
}

计数排序:

适用于数据量大但是数据范围小。如大型企业的数万员工年龄排序、快速得知高考名次等。

属于非比较类排序。

先确定这组 数据范围 为 0~k,数据量为 n 。分配 k +1个位置 , 用于 统计 0 至  k 在这组数据当中出现的次数

然后可以简化计数排序:使其空间复杂度为(k), 但是这样会使得排序算法变为不稳定

计数排序会用到 累加数组 ,累加数组中包含了两个信息:数出现次数 ,数的位置信息。此时可以使计数排序具有稳定性,且其空间复杂度不可避免的为(n+k)

 //计数排序
//需要已知这组数的 min,max
static void CountSort(int[] array, int min, int max)
{
int k = max - min + 1; // min~max 一共多少个数
int[] countArray = new int[k]; // 计数数组
int[] sortedArray = new int[array.Length]; //辅助数组

//遍历原序列 , 生成计数数组(包含计数信息 )
for (int i = 0; i < array.Length; i++)
{
countArray[array[i] - min]++;
}
//遍历计数数组 , 生成 累加计数数组 (包含计数信息、位置信息 )
for (int j = 1; j < k; j++)
{
countArray[j] = countArray[j] + countArray[j - 1];
}

//从后往前 遍历原序列array ,因为累加计数数组中包含的位置信息
//是原序列中“同类”元素中最后一个元素的位置信息,比如原序列
//中包含有三个 1 ,虽然三个 1 大小相等,但是位置不同,根据
//位置可以将元素区分为1_A, 1_B, 1_C。累加计数数组中是包含位置信息
//的,但是包含是1_C的位置信息,只有从后往前遍历原序列,然后根据
//累加计数数组将序列中的元素放入正确位置,才能保证排序算法的稳定性
//否则,会丧失稳定性,使序列“1_A, 1_B, 1_C”,在排序之后变成:
//“1_C, 1_B, 1_A”
for (int l = array.Length - 1; l >= 0; l--)
{
sortedArray[countArray[array[l] - min] - 1] = array[l];
countArray[array[l] - min]--;
}

//sortedArray中的数据是有序的,赋值给 array
for (int m = 0; m < array.Length; m++)
{
array[m] = sortedArray[m];
}
}

基数排序:

基数排序与计数排序十分类似,都是“非比较”、“归类”的排序。

需要找出原始序列的最大值,以此确定基数个数。假定最大为1000,则基数数量为 4,四个基数为“千位、百位、十位、个位”。

我们从个位开始,利用“累加计数数组”,对原数组按照个位的大小顺序对原序列进行排序,且需要保证稳定性。然后循环“基数数量”次,分别对剩下的“十位”、“百位”、“千位”进行同逻辑的排序。

从个位开始而不是千位的原因:需要从影响力小的基数开始,否则需要使用递归。

//基数排序
static void RadixSort(int[] array)
{
//生成辅助数组,用于排序
int[] helpArray = new int[array.Length];

//找出原序列的最大值,以此确一共有几个基数
int max = 0;
for (int i = 0; i < array.Length; i++) {
if (array[i] > max) {
max = array[i];
}
}

//循环确定基数数量,即确定最大值一共有几位数
int radixNum = 0;
while (max != 0) {
max /= 10; radixNum++;
}

//循环基数数量次
for (int j = 1; j <= radixNum; j++)
{
//生成 计数数组
int[] countArray = new int[10];
for (int k = 0; k < array.Length; k++) {
int radix = (int)(array[k] % Math.Pow(10, j) / Math.Pow(10, j - 1));
countArray[radix]++;
}

//计数数组 转换为 累加计数数组
for (int l = 1; l < countArray.Length; l++) {
countArray[l] = countArray[l] + countArray[l - 1];
}

//类似计数排序,从后往前遍历原序列
for (int m = array.Length - 1; m >= 0; m--) {
int radix = (int)(array[m] % Math.Pow(10, j) / Math.Pow(10, j - 1));
helpArray[countArray[radix] - 1] = array[m];
countArray[radix]--;
}

//至此,helpArray已经将原数列按基数大小进行了稳定的排序,
//便将排序好的数组赋值给原序列
for (int n = 0; n < array.Length; n++) {
array[n] = helpArray[n];
}
}
}

桶排序:

计数排序、基数排序本质都是桶排序。

举报

相关推荐

0 条评论