0
点赞
收藏
分享

微信扫一扫

C#之十大排序算法


No.1冒泡排序

冒泡排序无疑是最为出名的排序算法之一,从序列的一端开始往另一端冒泡(你可以从左往右冒泡,也可以从右往左冒泡,看心情),依次比较相邻的两个数的大小(到底是比大还是比小也看你心情)

  • 结构简单易于理解
  • 时间复杂度O(n2)
  • C#的内置排序函数使用的并非冒泡而是快排

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int temp = 0;
int[] arr = new int[10];
bool isSort = true;
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();
//升排
//核心算法
for (i = 0; i < arr.Length - 1; i++)
{
isSort = true;
for (int j = 0; j < arr.Length - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
isSort = false;
}
}
if (isSort)
{
break;
}
}
//输出排序结果
Console.WriteLine("(冒泡法)升序排序后的数组:");
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();

//降排
//核心算法
for (i = 0; i < arr.Length - 1; i++)
{
isSort = true;
for (int j = 0; j < arr.Length - 1 - i; j++)
{
if (arr[j] < arr[j + 1])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
isSort = false;
}
}
if (isSort)
{
break;
}
}
//输出排序结果
Console.WriteLine("(冒泡法)降序排序后的数组:");
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}
}
}

No.2 选择排序

选择排序的思路是这样的:首先,找到数组中最小的元素,拎出来,将它和数组的第一个元素交换位置,第二步,在剩下的元素中继续寻找最小的元素,拎出来,和数组的第二个元素交换位置,如此循环,直到整个数组排序完成。

至于选大还是选小,这个都无所谓,你也可以每次选择最大的拎出来排,也可以每次选择最小的拎出来的排,只要你的排序的手段是这种方式,都叫选择排序。

  • 双层循环,时间复杂度和冒泡一模一样,都是O(n2)

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int temp = 0;
int[] arr = new int[10];
//bool isSort = true;
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();

//核心算法
for (int z = 0; z < arr.Length; z++)
{
int min = z;//最小元素的下标
for (int j = z + 1; j < arr.Length; j++)
{
if (arr[j] < arr[min])
{
min = j;//找最小值
}
}
//交换位置
temp = arr[z];
arr[z] = arr[min];
arr[min] = temp;
}
//输出排序结果
Console.WriteLine("排序结果:");
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}
}
}

No.3 插入排序

插入排序的思想和我们打扑克摸牌的时候一样,从牌堆里一张一张摸起来的牌都是乱序的,我们会把摸起来的牌插入到左手中合适的位置,让左手中的牌时刻保持一个有序的状态。

那如果我们不是从牌堆里摸牌,而是左手里面初始化就是一堆乱牌呢? 一样的道理,我们把牌往手的右边挪一挪,把手的左边空出一点位置来,然后在乱牌中抽一张出来,插入到左边,再抽一张出来,插入到左边,再抽一张,插入到左边,每次插入都插入到左边合适的位置,时刻保持左边的牌是有序的,直到右边的牌抽完,则排序完毕。

最好情况的时间复杂度是 O(n),最坏情况的时间复杂度是 O(n2),然而时间复杂度这个指标看的是最坏的情况,而不是最好的情况,所以插入排序的时间复杂度是 O(n2)。

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int[] arr = new int[10];
int n = arr.Length;
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();

//核心算法
for (int z = 1; z < n; ++z)
{
int value = arr[z];
int j = 0;//插入的位置
for (j = z - 1; j >= 0; j--)
{
if (arr[j] > value)
{
arr[j + 1] = arr[j];//移动数据
}
else
{
break;
}
}
arr[j + 1] = value; //插入数据
}
//输出排序结果
Console.WriteLine("排序结果:");
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}
}
}

No.4 希尔排序

希尔排序这个名字,来源于它的发明者希尔,也称作“缩小增量排序”,是插入排序的一种更高效的改进版本。

我们知道,插入排序对于大规模的乱序数组的时候效率是比较慢的,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支。

  • 可能你会问为什么区间要以 gap = gap*3 + 1去计算,其实最优的区间计算方法是没有答案的,这是一个长期未解决的问题,不过差不多都会取在二分之一到三分之一附近。

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int[] arr = new int[10];
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();

//核心算法
int length = arr.Length;
//区间
int gap = 1;
while (gap < length)
{
gap = gap * 3 + 1;
}
while (gap > 0)
{
for (int z = gap; z < length; z++)
{
int tmp = arr[z];
int j = z - gap;
//跨区间排序
while (j >= 0 && arr[j] > tmp)
{
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
gap = gap / 3;
}
//输出排序结果
Console.WriteLine("排序结果:");
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}
}
}

No.5 归并排序

归并字面上的意思是合并,归并算法的核心思想是分治法,就是将一个数组一刀切两半,递归切,直到切成单个元素,然后重新组装合并,单个元素合并成小数组,两个小数组合并成大数组,直到最终合并完成,排序完毕。

归并排序的核心思想是分治,分而治之,将一个大问题分解成无数的小问题进行处理,处理之后再合并

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i;
int[] arr = new int[10];
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();
sort(arr);//方法调用
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}

//核心算法
public static void sort(int[] arr)
{
int[] tempArr = new int[arr.Length];
sort(arr, tempArr, 0, arr.Length - 1);
}

/// <summary>
/// 归并排序
/// </summary>
/// <param name="arr">排序数组</param>
/// <param name="tempArr">临时存储数组</param>
/// <param name="startIndex">归并起始位置</param>
/// <param name="endIndex">归并终止位置</param>
private static void sort(int[] arr, int[] tempArr, int startIndex, int endIndex)
{
if (endIndex <= startIndex)
{
return;
}
//中部下标
int middleIndex = startIndex + (endIndex - startIndex) / 2;

//分解
sort(arr, tempArr, startIndex, middleIndex);
sort(arr, tempArr, middleIndex + 1, endIndex);
//归并
merge(arr, tempArr, startIndex, middleIndex, endIndex);
}

/// <summary>
/// 归并排序
/// </summary>
/// <param name="arr">排序数组</param>
/// <param name="tempArr">临时存储数组</param>
/// <param name="startIndex">归并起始位置</param>
/// <param name="middleIndex">归并中间位置</param>
/// <param name="endIndex">归并终止位置</param>
private static void merge(int[] arr, int[] tempArr, int startIndex, int middleIndex, int endIndex)
{
//复制要合并的数据
for (int s = startIndex; s <= endIndex; s++)
{
tempArr[s] = arr[s];
}

int left = startIndex;//左边首位下标
int right = middleIndex + 1;//右边首位下标
for (int k = startIndex; k <= endIndex; k++)
{
if (left > middleIndex)
{
//如果左边的首位下标大于中部下标,证明左边的数据已经排完了。
arr[k] = tempArr[right++];
}
else if (right > endIndex)
{
//如果右边的首位下标大于了数组长度,证明右边的数据已经排完了。
arr[k] = tempArr[left++];
}
else if (tempArr[right] < tempArr[left])
{
arr[k] = tempArr[right++];//将右边的首位排入,然后右边的下标指针+1。
}
else
{
arr[k] = tempArr[left++];//将左边的首位排入,然后左边的下标指针+1。
}
}
}
}
}

可以发现 merge 方法中只有一个 for 循环,直接就可以得出每次合并的时间复杂度为 O(n) ,而分解数组每次对半切割,属于对数时间 O(log n) ,合起来等于 O(log2n) ,也就是说,总的时间复杂度为 O(nlogn) 。

关于空间复杂度,其实大部分人写的归并都是在 merge 方法里面申请临时数组,用临时数组来辅助排序工作,空间复杂度为 O(n),而我这里做的是原地归并,只在最开始申请了一个临时数组,所以空间复杂度为 O(1)。

No.6 快速排序

快速排序的核心思想也是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。

单边扫描

快速排序的关键之处在于切分,切分的同时要进行比较和移动,这里介绍一种叫做单边扫描的做法。

我们随意抽取一个数作为基准值,同时设定一个标记 mark 代表左边序列最右侧的下标位置,当然初始为 0 ,接下来遍历数组,如果元素大于基准值,无操作,继续遍历,如果元素小于基准值,则把 mark + 1 ,再将 mark 所在位置的元素和遍历到的元素交换位置,mark 这个位置存储的是比基准值小的数据,当遍历结束后,将基准值与 mark 所在元素交换位置即可。

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int[] arr = new int[10];
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();
sort(arr);
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}
//核心算法
public static void sort(int[] arr)
{
sort(arr, 0, arr.Length - 1);
}

private static void sort(int[] arr, int startIndex, int endIndex)
{
if (endIndex <= startIndex)
{
return;
}
//切分
int pivotIndex = partition(arr, startIndex, endIndex);
sort(arr,startIndex, pivotIndex - 1);
sort(arr, pivotIndex + 1, endIndex);
}

private static int partition(int[] arr, int startIndex, int endIndex)
{
int pivot = arr[startIndex];//取基准值
int mark = startIndex;//Mark初始化为起始下标

for (int i = startIndex + 1; i <= endIndex; i++)
{
if (arr[i] < pivot)
{
//小于基准值 则mark+1,并交换位置。
mark++;
int p = arr[mark];
arr[mark] = arr[i];
arr[i] = p;
}
}
//基准值与mark对应元素调换位置
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
}
}

双边扫描

双边扫描的做法,看起来比较直观:我们随意抽取一个数作为基准值,然后从数组左右两边进行扫描,先从左往右找到一个大于基准值的元素,将下标指针记录下来,然后转到从右往左扫描,找到一个小于基准值的元素,交换这两个元素的位置,重复步骤,直到左右两个指针相遇,再将基准值与左侧最右边的元素交换。

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int[] arr = new int[10];
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();
sort(arr);
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}
//核心算法
public static void sort(int[] arr)
{
sort(arr, 0, arr.Length - 1);
}

private static void sort(int[] arr, int startIndex, int endIndex)
{
if (endIndex <= startIndex)
{
return;
}
//切分
int pivotIndex = partition(arr, startIndex, endIndex);
sort(arr, startIndex, pivotIndex - 1);
sort(arr, pivotIndex + 1, endIndex);
}


private static int partition(int[] arr, int startIndex, int endIndex)
{
int left = startIndex;
int right = endIndex;
int pivot = arr[startIndex];//取第一个元素为基准值
int temp;
while (true)
{
//从左往右扫描
while (arr[left] <= pivot)
{
left++;
if (left == right)
{
break;
}
}

//从右往左扫描
while (pivot < arr[right])
{
right--;
if (left == right)
{
break;
}
}

//左右指针相遇
if (left >= right)
{
break;
}

//交换左右数据
temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}

//将基准值插入序列
temp = arr[startIndex];
arr[startIndex] = arr[right];
arr[right] = temp;
return right;
}
}
}

快速排序的时间复杂度和归并排序一样,O(n log n),但这是建立在每次切分都能把数组一刀切两半差不多大的前提下,如果出现极端情况,比如排一个有序的序列,如[ 9,8,7,6,5,4,3,2,1 ],选取基准值 9 ,那么需要切分 n - 1 次才能完成整个快速排序的过程,这种情况下,时间复杂度就退化成了 O(n2),当然极端情况出现的概率也是比较低的。

所以说,快速排序的时间复杂度是 O(nlogn),极端情况下会退化成 O(n2),为了避免极端情况的发生,选取基准值应该做到随机选取,或者是打乱一下数组再选取。

另外,快速排序的空间复杂度为 O(1)

No.7 堆排序

堆排序顾名思义,是利用堆这种数据结构来进行排序的算法。

如果你了解堆这种数据结构,你应该知道堆是一种优先队列,两种实现,最大堆和最小堆,由于我们这里排序按升序排,所以就直接以最大堆来说吧。

我们完全可以把堆(以下全都默认为最大堆)看成一棵完全二叉树,但是位于堆顶的元素总是整棵树的最大值,每个子节点的值都比父节点小,由于堆要时刻保持这样的规则特性,所以一旦堆里面的数据发生变化,我们必须对堆重新进行一次构建。

既然堆顶元素永远都是整棵树中的最大值,那么我们将数据构建成堆后,只需要从堆顶取元素不就好了吗? 第一次取的元素,是否取的就是最大值?取完后把堆重新构建一下,然后再取堆顶的元素,是否取的就是第二大的值? 反复的取,取出来的数据也就是有序的数据。

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int[] arr = new int[10];
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();
sort(arr);
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}

//核心算法
public static void sort(int[] arr)
{
int length = arr.Length;
//构建堆
buildHeap(arr, length);
for (int i = length - 1; i > 0; i--)
{
//将堆顶元素与末位元素调换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
//数组长度-1 隐藏堆尾元素
length--;
//将堆顶元素下沉 目的是将最大的元素浮到堆顶来
sink(arr, 0, length);
}
}
private static void buildHeap(int[] arr, int length)
{
for (int i = length / 2; i >= 0; i--)
{
sink(arr, i, length);
}
}

/// <summary>
/// 下沉调整
/// </summary>
/// <param name="arr">arr 数组</param>
/// <param name="index">index 调整位置</param>
/// <param name="length">length 数组范围</param>
private static void sink(int[] arr, int index, int length)
{
int leftChild = 2 * index + 1;//左子节点下标
int rightChild = 2 * index + 2;//右子节点下标
int present = index;//要调整的节点下标

//下沉左边
if (leftChild < length && arr[leftChild] > arr[present])
{
present = leftChild;
}

//下沉右边
if (rightChild < length && arr[rightChild] > arr[present])
{
present = rightChild;
}

//如果下标不相等 证明调换过了
if (present != index)
{
//交换值
int temp = arr[index];
arr[index] = arr[present];
arr[present] = temp;

//继续下沉
sink(arr, present, length);
}
}
}
}

  • 堆排序和快速排序的时间复杂度都一样是 O(nlogn)

No.8 计数排序

计数排序是一种非基于比较的排序算法,我们之前介绍的各种排序算法几乎都是基于元素之间的比较来进行排序的,计数排序的时间复杂度为 O(n + m ),m 指的是数据量,说的简单点,计数排序算法的时间复杂度约等于 O(n),快于任何比较型的排序算法。

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int[] arr = new int[10];
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();
sort(arr);
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}

//核心算法
public static void sort(int[] arr)
{
//找出数组中的最大值
int max = arr[0];
for (int i = 1; i < arr.Length; i++)
{
if (arr[i] > max)
{
max = arr[i];
}
}
//初始化计数数组
int[] countArr = new int[max + 1];

//计数
for (int i = 0; i < arr.Length; i++)
{
countArr[arr[i]]++;
arr[i] = 0;
}

//排序
int index = 0;
for (int i = 0; i < countArr.Length; i++)
{
if (countArr[i] > 0)
{
arr[index++] = i;
}
}
}
}
}

No.9 基数排序(桶排序)

桶排序可以看成是计数排序的升级版,它将要排的数据分到多个有序的桶里,每个桶里的数据再单独排序,再把每个桶的数据依次取出,即可完成排序。

基数排序可以看成桶排序的扩展,也是用桶来辅助排序

using System;

namespace Sort
{
class Program
{
static void Main(string[] args)
{
int i = 0;
int[] arr = new int[10];
Random arr1 = new Random();
Console.WriteLine("原数组:");
for (i = 0; i < arr.Length; i++)
{
arr[i] = arr1.Next(1000);
Console.Write(arr[i] + "\t");
}
Console.WriteLine();
RadixSort(arr);
foreach (int x in arr)
{
Console.Write(x + "\t");
}
Console.WriteLine();
}
public static int[] RadixSort(int[] arr)
{
int maxDigit = 0;
int digit, value; // 1代表个位,2代表十位,3代表百位。。。
for (int i = 0; i < arr.Length; i++) // 计算数组中最高位
{
digit = 0;
value = arr[i];
while (value != 0)
{
value /= 10;
digit++;
}
if (digit > maxDigit)
{
maxDigit = digit;
}
}
RadixSortCore(arr, maxDigit);
return arr;
}

private static void RadixSortCore(int[] arr, int maxDigit)
{
int[][] buckets = new int[10][];
int[] helpers = new int[10]; // 每次排序时,对应位上数组的长度
int k = 0;
int mod = 10, dev = 1;
for (int i = 1; i <= maxDigit; i++, mod *= 10, dev *= 10) // 每次排序,都类似于桶排序
{
for (int j = 0; j < buckets.Length; j++)
{
helpers[j] = 0;
}

for (int j = 0; j < arr.Length; j++)
{
k = (arr[j] % mod) / dev;
helpers[k]++;
}

for (int j = 0; j < buckets.Length; j++)
{
buckets[j] = new int[helpers[j]];
helpers[j] = 0;
}

for (int j = 0; j < arr.Length; j++)
{
k = (arr[j] % mod) / dev;
buckets[k][helpers[k]++] = arr[j];
}

int arrIndex = 0;
for (int j = 0; j < buckets.Length; j++)
{
if (buckets[j].Length > 0)
{
for (int z = 0; z < buckets[j].Length; z++)
{
arr[arrIndex++] = buckets[j][z];
}
}
}
}
}
}
}

其实它的思想很简单,不管你的数字有多大,按照一位一位的排,0 - 9 最多也就十个桶:先按权重小的位置排序,然后按权重大的位置排序

当然,如果你有需求,也可以选择从高位往低位排


举报

相关推荐

0 条评论