您的位置:首页 > 编程语言 > C#

常见排序方法总结和C#实现

2021-02-24 23:56 981 查看

本文动图来源:https://www.cnblogs.com/aishangJava/p/10092341.html

排序往往会用到循环或者递归,对于循环和递归,第一步分析并实现循环或递归的情况(需要多少层循环,每一层循环的起始点分别是什么;是否使用递归,递归怎么分解),第二步分析在最内层循环或递归终止条件中每一步操作会遇到几种情况,然后分情况去实现。

一.冒泡排序

动图:

 

分析:冒泡排序将数据分为已排序区和未排序区,每次在未排序区中依次遍历,判断相邻两个的大小看是否需要交换,这样当前未排序区中的最大或最小值就能不断移动,最后遍历完成当前最值也移动到了未排序区和已排序区中间,然后更新未排序去和已排序区。冒泡排序分为两层循环,外层循环i从0号到n-1号进行遍历,n-i就是未排序区和已排序区分界位置,内层循环j从0号到n-i-2号,遍历未排序区,每次循环都判断相邻的j号和j+1号的大小,根据大小有两种情况,一种交换一种不交换。

实现:

/// <summary>
/// 冒泡排序
/// </summary>
/// <param name="n">待排序的数组</param>
static void Sort1(ref int[] n)
{
//是否结束循环的标签,如果一次遍历下来都没有发生数字交换,说明数字已经排好序了
//每次进入外层循环中,将初始值置为true,一旦发生数字交换将值同时置为false
bool end;
//双重循环
//外层0=>length-2,内层0=>length-2-i
//依次遍历,将相邻的两个值进行比较,如果大小不合适就交换
for (int i = 0; i < n.Length - 1; i++)
{
end = true;
for (int j = 0; j < n.Length - i - 1; j++)
{
//判断相邻两个值的大小并排序,当前判断下来是由大到小排序,如果想由小到大排序,只需将小于改为大于即可
if (n[j] < n[j + 1])
{
Switch2Int32Number(ref n[j], ref n[j + 1]);
end = false;
}
}
//根据是否发生交换决定是否结束函数运行
if (end)
{
return;
}
}
}
/// <summary>
/// 交换两个int类型的数字
/// </summary>
/// <param name="i"></param>
/// <param name="j"></param>
static void Switch2Int32Number(ref int i,ref int j)
{
i = i + j;
j = i - j;
i = i - j;
}

二选择排序

动图:

 

 分析:选择排序将数据分为未排序区和已排序区,每次遍历未排序区,找到当前最值的下标,然后将当前最值和未排序区边上的数据进行交换,更新已排序区和未排序区。需要有两层循环,外层循环i从0到n-1,n-i是未排序区和已排序区的边界,内层循环j从0到n-i-1号,遍历未排序区,使用一个变量index记录当前循环找到的最值的下标,每次都将当前的最值和数组中的值进行比较,根据大小分为两类,一类需要更新index下标,一类不需要。

实现:

/// <summary>
/// 选择排序
/// </summary>
/// <param name="n">待排序的数组</param>
static void Sort2(ref int[] n)
{
//记录当前找到的最大值或最小值的下标
int index = 0;
//双重循环,外层循环从0=>length-1,内层循环从0=>length-i
for (int i = 0; i < n.Length; i++)
{
//重置当前最值的下标
index = 0;
for (int j = 0; j < n.Length - i; j++)
{
//遍历判断找到当前轮的最值下标
//当前是由小到大排序,如果由大到小排序将>改为<即可
if(n[j] > n[index])
{
index = j;
}
}
//判断当前需要交换的位置是否就是最值,不是再交换,节约性能
if(n.Length - i - 1 != index)
{
Switch2Int32Number(ref n[n.Length - i - 1], ref n[index]);
}
}
}
/// <summary>
/// 交换两个int类型的数字
/// </summary>
/// <param name="i"></param>
/// <param name="j"></param>
static void Switch2Int32Number(ref int i,ref int j)
{
i = i + j;
j = i - j;
i = i - j;
}

三.插入排序

动图:

 

 分析:插入排序左边部分为已排序区,右边部分为未排序区,每次将未排序区最左边的值插入左边已排序区中合适位置,然后更新已排序区和未排序区。需要两层循环,外层循环i从1到n-1,代表已排序区和未排序区的分界线,内层循环j从i-1到-1,反向遍历未排序区,使用临时变量记录需要插入的数据,j+1为当前待插入位置的下标,j为前一个数据的下标,依次和前一个下基准数据判断,有3种情况:可以插入则插入并终止内层循环,不能插入则将前一个数据往后移动并继续循环,j为-1则说明遍历完了直接插入。

实现:

/// <summary>
/// 插入排序
/// </summary>
/// <param name="n">待排序的数组</param>
static void Sort3 (ref int[] n)
{
//记录当前要排序的数字
int nowSortNum;
//双重循环,外层循环i从1=>length-1,内层循环j从i-1=>-1
//j+1指当前要排序的数字待插入位置的下标,j是被比较数字的下标
//当前要排序的数字和前一个数字(下标j)比较,有三种情况
//1.前一个数字下标j为-1,说明比较到头了,直接将当前数字插入到当前待插入位置
//2.前一个数字比待插入数字大(也可能是小,看你是从大到小排序还是从小到大排序),说明不插入当前位置,将刚才被比较的数字移到当前待插入的位置,继续循环,待插入的位置往前移
//3.前一个数字比待插入数字小或相等(也可能是大于等于),说明插入当前位置,且需要结束循环
for (int i = 1; i < n.Length; i++)
{
//记录要排序的数字
nowSortNum = n[i];
for (int j = i - 1; j >= -1; j--)
{
//如果前一个数字下标为-1,说明比较完了,直接将数字插入当前位置
if(j == -1)
{
n[j + 1] = nowSortNum;
}
//根据大小判断是否插入当前位置还是移动被比较数字
//当前是从小到大排序,如果从大到小排序,将<改成>即可
else if(nowSortNum < n[j])
{
n[j + 1] = n[j];
}
//如果将待插入数字插入了数组中,直接结束这轮内层for循环
else
{
n[j + 1] = nowSortNum;
break;
}
}
}
}

四.希尔排序

动图:

 

分析:希尔排序是插入排序的升级版本,在插入排序的外层添加一层循环,使用步长对数据进行分组,如步长为3,则模三余0的下标0、3、6、9等一组,模三余1的下标1,4,7,10等一组,模三余2的下标2,5,8,11等一组。分组后对组内的数据进行插入排序,然后模长变为原来的一半,知道模长为0。

实现:

/// <summary>
/// 希尔排序
/// </summary>
/// <param name="n">待排序数组</param>
static void Sort4(ref int[] n)
{
int nowSortNum;
//希尔排序是插入排序的升级版,在插入排序的基础上使用步长进行分组,每次排序移动步长个单位,降低了极端情况下的时间复杂度
for(int step = n.Length / 2;step > 0;step /= 2)
{
for(int i = step;i < n.Length;i++)
{
nowSortNum = n[i];
for(int j = i;j >= 0;j -= step)
{
if(j - step < 0)
{
n[j] = nowSortNum;
}
else if(nowSortNum < n[j - step])
{
n[j] = n[j - step];
}
else
{
n[j] = nowSortNum;
break;
}
}
}
}
}

 五.归并排序

动图:

 

 

 分析:归并排序分为递归加上合并。首先递归是将大规模问题分解为小规模问题,这里的递归将数组平分为两个数组,然后递归这两个数组得到两个数组分别排序的结果,递归的终止条件是数组元素个数等于1。合并是指递归得到的两个排好序的子数组并不是直接合起来的数组就是排好序的,还需要将两个数组按照顺序合并,同时遍历两个数组,按照从小到大或者从大到小的顺序将数据放入新数组中完成合并。合并的过程分为3种情况,左边数组放完了之后将右边数组依次放入新数组,右边数组放完了之后将左边数组依次放入新数组,两边数组都没有放完时判断左边数组和右边数组的当前需要放入值的大小,将大的或者小的放入新数组。

实现:

/// <summary>
/// 归并排序
/// </summary>
/// <param name="n">待排序的数组</param>
static void Sort5(ref int[] n)
{
//递归终止条件
if(n.Length == 1)  return;
//计算数组的剪切点
int cutPoint = n.Length / 2;
//将数组平分为两个数组并
3ff8
赋值
int[] n1 = new int[cutPoint];
int[] n2 = new int[n.Length - cutPoint];
for (int i = 0; i < n1.Length; i++)
{
n1[i] = n[i];
}
for (int i = 0; i < n2.Length; i++)
{
n2[i] = n[cutPoint + i];
}
//递归调用这个函数给两个分数组排序
Sort5(ref n1);
Sort5(ref n2);
//两个分数组排好序后需要同时遍历并填入原数组n中,新建两个变量记录当前遍历到的下标位置
int n1Index = 0;
int n2Index = 0;
//为原数组n赋值,赋值时有三种情况
for (int i = 0; i < n.Length; i++)
{
//如果n1遍历完了,将n2的元素填入n中
if(n1Index == n1.Length)
{
n[i] = n2[n2Index];
n2Index++;
}
//如果n2遍历完了,将n1的元素填入n中
else if(n2Index == n2.Length)
{
n[i] = n1[n1Index];
n1Index++;
}
//如果两个数组都没有遍历完,需要根据两个数组中当前的元素大小决定谁赋值,又分为两种小情况
else
{
//这里实现了从小到大排序,如果要从大到小排序将<改为>即可
if(n1[n1Index] < n2[n2Index])
{
n[i] = n1[n1Index];
n1Index++;
}
else
{
n[i] = n2[n2Index];
n2Index++;
}
}
}
}

六.快速排序

动图:

 

 

 分析:上方的动图可以参考。我并未按照上方图片中的方法实现,但是核心思想是一样的。将最右侧数字置为基准(pivot)并取出保存,这个基准就是我们首先排好序的数字,然后给定左右游标(L和R),分别指向最左侧和最右侧。先判断左游标指向的数字是否小于基准,如果小于基准就说明这个数字应该在基准的左边,左游标右移,反之说明这个数字应该在基准的右边,将这个数字移动到右游基准位置;然后判断右游标指向的数字是否大于基准,如果大于基准说明这个数字应该在基准右边,右游标左移,反之说明数字应该在基准左边,将右游标指向的数字移动到左游基准位置;之后循环又判断左游标......直到左右游基准位置重合为止,重合的位置就是基准应该放入的位置,将基准放好。基准放好后就实现了基准左边的数字都比其小,右边的数字都比其大,然后递归基准左右两边的数字即可。递归的终止条件是给定要排序的数字只有一个。

实现:

/// <summary>
/// 将数组的left位到right位排序(闭区间)
/// </summary>
/// <param name="n">待排序的数组</param>
/// <param name="left"></param>
/// <param name="right"></param>
static void Sort6(int[] n,int left,int right)
{
//如果左数字大于等于右数字,终止循环
if (left >= right) return;
//定义和赋值左游标、右游标、基准
int tempLeft = left;
int tempRight = right;
int p = n[right];
//在左右游标没有相遇的时候进入循环
while (tempLeft != tempRight)
{
//游标有可能移过了,所以需要满足左游标小于右游标的情况下判断是否移动
while(tempLeft < tempRight && n[tempLeft] <= p)
{
tempLeft++;
}
//左游标移动完成后就将左游标移动到右游标的位置
n[tempRight] = n[tempLeft];
//同理移动右游标并在移动完成后将右游标值移动到左游标的位置
while(tempLeft < tempRight && n[tempRight] >= p)
{
tempRight--;
}
n[tempLeft] = n[tempRight];
}
//跳出循环时一定是左右游标重合,此时放下基准值
n[tempRight] = p;
//左右进行递归
Sort6(n, left, tempRight - 1);
Sort6(n, tempLeft + 1, right);
}

 七.堆排序

动图:

 

 分析:对于一个完全二叉树,父节点小于等于所有子节点的二叉树称为小顶堆,父节点大于等于所有子节点的二叉树称为小顶堆。不难发现,大顶堆的根节点一定是最大值,小顶堆的根节点一定是最小值,因此可以使用不断将二叉树重新构建成大顶堆或者小顶堆的方式取得数组的最大值或者最小值,然后将根节点值和最后索引值交换,继续构建大顶堆或者小顶堆找最值直到找完。首先将数组按照二叉树的方式构建成大顶堆,然后进行双重循环,外层循环i从length=>1,是已排序区和未排序区的分界点,i也是当前需要和堆顶交换位置的数字下标;交换后内层循环将二叉树再次构建成大顶堆。构建大顶堆的过程有3种情况:1.已经是大顶堆,不需要动;2.左叶子节点值最大,将左叶子节点和父节点交换,继续左叶子节点构建大顶堆;3.右叶子节点值最大,同样交换继续构建。构建大顶堆的节点有3种情况:1.没有子节点;2.只有左子节点;3.有左右子节点(因为是完全二叉树,不存在只有右子节点的情况)

实现:

/// <summary>
/// 堆排序
/// </summary>
/// <param name="n">待排序数组</param>
static void Sort7(int[] n)
{
//记录构建大顶堆过程中发生了交换的叶子节点的索引
int index;
//将数组排序成大顶堆
for (int i = n.Length / 2 + 1; i > 0; i--)
{
//重置索引,从第i个开始交换
index = i;
while(index != -1)
{
index = GetHeap(n, n.Length, index);
}
}
//依次遍历,将堆顶和未排序区间的最后一个数交换,然后继续将未排序区排成大顶堆
for (int i = n.Length; i > 1; i--)
{
Switch2Int32Number(ref n[0], ref n[i - 1]);
//重置索引,从第1个开始交换
index = 1;
while(index != -1)
{
index = GetHeap(n, i - 1, index);
}
}
}
/// <summary>
/// 根据传入的父节点的索引值将父节点及其子节点构建成大顶堆
/// </summary>
/// <param name="n">二叉树数组</param>
/// <param name="maxIndex">未排序区的边界下标</param>
/// <param name="father">父节点在二叉树中的索引</param>
/// <returns>和父节点进行交换的子节点索引,没有交换返回-1</returns>
static int GetHeap(int[] n,int maxIndex,int father)
{
//外层if判断节点情况,内层if判断节点之间的大小
//没有叶子节点
if (father * 2 > maxIndex)
{
return -1;
}
//只有左叶子节点
else if (father * 2 == maxIndex)
{
//判断父节点和左叶子节点的大小
if (n[father - 1] < n[father * 2 - 1])
{
Switch2Int32Number(ref n[father - 1], ref n[father * 2 - 1]);
return father * 2;
}
else
return -1;
}
//有左右叶子节点
else
{
//判断父节点和两个叶子节点中谁最大
if (n[father - 1] <= n[father * 2 - 1] && n[father * 2 - 1] >= n[father * 2])
{
Switch2Int32Number(ref n[father - 1], ref n[father * 2 - 1]);
return father * 2;
}
else if (n[father - 1] <= n[father * 2] && n[father * 2 - 1] <= n[father * 2])
{
Switch2Int32Number(ref n[father - 1], ref n[father * 2]);
return father * 2 + 1;
}
else
return -1;
}
}
/// <summary>
/// 交换两个int类型的数字
/// </summary>
/// <param name="i"></param>
3ff8
;
/// <param name="j"></param>
static void Switch2Int32Number(ref int i,ref int j)
{
i = i + j;
j = i - j;
i = i - j;
}

 八.计数排序

动图:

 

 分析:计数排序的基本原理是利用一个新的数组,将原数组的元素依次存储到新数组中,存储的方式为:如原数组有元素12,就将新数组中下标为12的元素的值加一。存储的结果是新数组中的元素下标对应原来数组中的元素值,新数组中元素值对应原数组中元素的个数。存储完成后遍历新数组,反向将值存回原数组即可。

实现:

/// <summary>
/// 计数排序
/// </summary>
/// <param name="n">待排序的数组</param>
static void Sort8(int[] n)
{
//数组没有值不用排序
if (n.Length == 0) return;
//记录数组中的最大值和最小值
int max = n[0];
int min = n[0];
//遍历找到数组中的最大和最小值
for (int i = 0; i < n.Length; i++)
{
if (n[i] > max)
max = n[i];
if (n[i] < min)
min = n[i];
}
//创建新数组,长度为max - min + 1,因为数组中的值包括最大和最小(闭区间)
//存储时将数值减去最小值得到应该存储的下标位置,取出时反过来
int[] newN = new int[max - min + 1];
//存储数组
for (int i = 0; i < n.Length; i++)
{
newN[n[i] - min]++;
}
//记录给原数组赋值到哪个下标了
int index = 0;
//遍历新数组为原数组赋值
for (int i = 0; i < newN.Length; i++)
{
//新数组中的值为几,原数组中就有几个这个数字,赋值后下标index递增,新数组中的值递减直到0
while(newN[i] > 0)
{
n[index++] = i + min;
newN[i]--;
}
}
}

九.基数排序

动图:

 

分析:基数排序按照数字的每一位分别排序,先按照个位排序,再按照十位排序......以此类推,直到每一位都排序完成。显然,基数排序必然需要1个10个元素的数组,数组元素是10个存储数据的容器(数组、队列等),如按照个位排序,将数字按照个位数字分别存储入相应下标的容器内,然后按照下标顺序将容器内的数字一一存回原数组;然后继续按照十位做一次操作......。

实现:

/// <summary>
/// 基数排序
/// </summary>
/// <param name="n">待排序的数组</param>
static void Sort9(int[] n)
{
//数组没有值不用排序
if (n.Length == 0) return;
//记录数组中的最大值和最小值
int max = n[0];
int min = n[0];
//遍历找到数组中的最大和最小值
for (int i = 0; i < n.Length; i++)
{
if (n[i] > max)
max = n[i];
if (n[i] < min)
min = n[i];
}
//考虑到负数的情况,如果最小值为负,则将数组中所有数变成正数,最大值也相应变大
if(min < 0)
{
for (int i = 0; i < n.Length; i++)
{
n[i] -= min;
}
max -= min;
}
//记录max是几位数
int maxDigit = 0;
//用于计算max位数的临时变量
int countDigit = 1;
//遍历找max是几位数
while(max/countDigit != 0)
{
maxDigit++;
countDigit *= 10;
}
//考虑到每个桶内的值先进先出的特征,这里可以选择队列作为桶进行存储
Queue<int>[] queues = new Queue<int>[10];
//初始化队列数组(初始化所有桶)
for (int i = 0; i < queues.Length; i++)
{
queues[i] = new Queue<int>();
}
//记录桶下标的变量
int queueIndex;
//记录数组下标的变量
int nIndex;
//临时变量,用于计算当前位的下标
int temp;
//循环将数字放入桶中,再放回数组
//i代表当前根据哪一位进行放入放回操作,如i为1代表按照第一位(个位)的数字进行放入放回操作
for (int i = 1; i <= maxDigit; i++)
{
//为临时变量赋值,如计算十位数字,需要将原数字模100取余得到个位和十位再除以10取商得到十位数
temp = (int)Math.Pow(10, i - 1);
//遍历数组n,计算每一个数字的相应位,根据这一位的值将数字放入相应的队列(桶)中
for (int j = 0; j < n.Length; j++)
{
queueIndex = n[j] % (temp * 10) / temp;
queues[queueIndex].Enqueue(n[j]);
}
//初始化n的下标
nIndex = 0;
//遍历队列数组,如果当前的队列中有值,将值一一存回数组n中
for (queueIndex = 0; queueIndex < 10; queueIndex++)
{
whil
1a92
e (queues[queueIndex].Count > 0)
{
n[nIndex] = queues[queueIndex].Dequeue();
nIndex++;
}
}
}
//排序完成后,如果这个数组是有负值的情况,需要将数组值还原
if (min < 0)
{
for (int i = 0; i < n.Length; i++)
{
n[i] += min;
}
}
}

十.桶排序

动图:

 

 分析:这里对桶排序不做实现。桶排序可以理解为利用分治法进行排序,先初始化n个桶,然后按照一定的函数映射规则将数据进行分类并放入桶中,使第一个桶中的数字都比第二个桶中的数字小,第二个桶中数字比第三个桶中的数字小......以此类推,这样就可以利用其他排序方法分别对每个桶排序,然后按照顺序一一从桶中取出所有排好序的数,就得到了最终的排序结果。可以看到,桶排序的核心就是进行了一次分类,使一定区间的数字落到相应的桶内,像快速排序可以理解为是一种桶排序,排完一次序后将数字分为两个桶再分别使用快速排序进行排序;计数排序可以理解为每个数字都是一个桶的桶排序,只是有些桶没有使用,造成了空间浪费;基数排序也有10个桶,但是和桶排序的过程还差得有点远,它是不断将数字按照一定规则分别放入桶中又按照一定顺序拿出来进行的排序;归并排序也可以理解为有两个桶,只是没有进行分类,在将数字拿出桶时进行了排序的算法。

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: