口诀:
外层循环 n-1;内层循环 n-1-i;两两比较做互换;
以下代码在控制台操作,输入数字以‘逗号’隔开
Console.WriteLine("请输入一组数字:");
string str = Console.ReadLine();
string[] num = str.Split(','); // 获取到每个数
int[] numlist=new int[num.Length];
for (int i = 0; i < num.Length; i++)
{
numlist[i] = int.Parse(num[i]); // 将每个数存储到numlist数组中
}
for (int i = 0; i < numlist.Length-1; i++)
{
for (int j = 0; j < numlist.Length-1-i; j++)
{
if (numlist[j]<numlist[j+1]) //此数小于后一个数,两个数交换,从大到小排列
{
int temp = numlist[j];
numlist[j] = numlist[j + 1];
numlist[j + 1] = temp;
}
}
}
Console.WriteLine("排序结果为:");
for (int i = 0; i < numlist.Length; i++)
{
Console.Write(numlist[i]+",");
}
Console.ReadLine();
int[] numlist = { 3, 6, 88, 35, 1, 45, 7 };
Console.WriteLine("排列前一组数字:");
for (int i = 0; i < numlist.Length; i++)
{
Console.Write(" " + numlist[i]);
}
for (int i = 1; i < numlist.Length; i++)
{
int temp = numlist[i];
for (int j = i-1; j >=0; j--) // 遍历排在此数前面的数
{
if (numlist[j]>temp) // 如果前面的一个数大于此数,交换位置,知道前面数比此数小为止
{
numlist[j + 1] = numlist[j];
numlist[j] = temp;
}
else
{
break;
}
}
}
Console.WriteLine("\r\n"+"排序结果为:");
for (int i = 0; i < numlist.Length; i++)
{
Console.Write(" "+numlist[i]);
}
Console.ReadLine();
int[] numlist = { 33, 2, 6, 89, 11 };
Console.WriteLine("排列前一组数字:");
for (int i = 0; i < numlist.Length; i++)
{
Console.Write(" "+numlist[i]);
}
//总共要进行n-1轮比较
for (int i = 0; i < numlist.Length-1; i++)
{
int min =i;
//每轮需要比较的次数n-i
for (int j = i+1; j < numlist.Length; j++)
{
if (numlist[min]>numlist[j]) // 当此数大于后一个数
{
// 记录目前能找到的最小值元素的下标
min = j;
}
}
int temp = numlist[i];
numlist[i] = numlist[min];
numlist[min] = temp;
}
Console.WriteLine("\r\n"+"排序后结果为:");
for (int i = 0; i < numlist.Length; i++)
{
Console.Write(" "+numlist[i]);
}
Console.ReadLine();
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
步骤为:
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个演算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace 快速排序 //***对相同元素, 不稳定的排序算法***
{
//相对来说,快速排序数值越大速度越快 。 快速排序是所有排序里面最快的
class Program
{
static void Main(string[] args)
{
int[] arr = { 15, 22, 35, 9, 16, 33, 15, 23, 68, 1, 33, 25, 14 }; //待排序数组
QuickSort(arr, 0, arr.Length - 1); //调用快速排序函数。传值(要排序数组,基准值位置,数组长度)
//控制台遍历输出
Console.WriteLine("排序后的数列:");
foreach (int item in arr)
Console.WriteLine(item);
}
private static void QuickSort(int[] arr, int begin, int end)
{
if (begin >= end) return; //两个指针重合就返回,结束调用
int pivotIndex = QuickSort_Once(arr, begin, end); //会得到一个基准值下标
QuickSort(arr, begin, pivotIndex - 1); //对基准的左端进行排序 递归
QuickSort(arr, pivotIndex + 1, end); //对基准的右端进行排序 递归
}
private static int QuickSort_Once(int[] arr, int begin, int end)
{
int pivot = arr[begin]; //将首元素作为基准
int i = begin;
int j = end;
while (i < j)
{
//从右到左,寻找第一个小于基准pivot的元素
while (arr[j] >= pivot && i < j) j--; //指针向前移
arr[i] = arr[j]; //执行到此,j已指向从右端起第一个小于基准pivot的元素,执行替换
//从左到右,寻找首个大于基准pivot的元素
while (arr[i] <= pivot && i < j) i++; //指针向后移
arr[j] = arr[i]; //执行到此,i已指向从左端起首个大于基准pivot的元素,执行替换
}
//退出while循环,执行至此,必定是 i= j的情况(最后两个指针会碰头)
//i(或j)所指向的既是基准位置,定位该趟的基准并将该基准位置返回
arr[i] = pivot;
return i;
}
}
}
插入排序的升级
static void Main(string[] args)
{
int[] numlist = { 33, 6, 89, 34, 63, 2, 7 };
Console.WriteLine("排序前数组:");
for (int i = 0; i < numlist.Length; i++)
{
Console.Write(" "+numlist[i]);
}
// 将每组间隔为h的分组进行排序,刚开始h=数组长度的一半
for (int h = numlist.Length/2; h >0; h/=2)
{
//对每组进行插入排序
for (int i = h; i < numlist.Length; i++)
{
// 将numlist[i]插入到所在分组的正确位置上
insertI(numlist, h, i);
}
}
Console.WriteLine("\r\n排序后结果:");
for (int i = 0; i < numlist.Length; i++)
{
Console.Write(" "+numlist[i]);
}
Console.ReadLine();
}
// 插入排序的方法
private static void insertI(int[] arr,int h, int i)
{
int temp = arr[i];
int k;
for ( k= i-h; k>=0 && temp<arr[k]; k-=h)
{
arr[k + h] = arr[k];
}
arr[k + h] = temp;
}
堆排序首先是构建最大堆或最小堆。最大堆是用来正序排序,最小堆是用来倒序排序。
最大堆是指二叉树中每个结点的值都比其左右子结点的值大。同理最小堆是指二叉树中每个结点的值都比其左右子结点的值小。
对于二叉树不了解,在这里可以只有一个印象就可以。二叉树就是一个结点最多只有两个左右子结点。至于什么是完全二叉树,这里就不在过多解释,以后有机会写数据结构的时候,会着重解释,但是有一点要知道,数列从上往下,从左往右,按照只有一个根结点,且每个结点有两个子结点这样构建二叉树,那么他就是一颗完全二叉树。
下面我用一张图,来表示上面的概念,并加深印象。
完全二叉树:
可以发现其实每个结点的下标和其左右子结点的下标是有一定关系的,即结点下标为n,左子结点下标为:2n+1,右子结点的下标为:2n+2。
可以看出因为根结点要比左右子结点数值大,而且其左右子结点要比其孙子结点数值大,以此类推,此时的根结点即为数列的最大值。
那么我们如何把一个无序构建成一个最大堆。首先看最大堆的最大特点就是:父结点的数值一定比左右结点数值大,我们依照这个规则不断的调整结点使其满足条件即可。
再仔细观察堆我们发现,由一半以上的结点是没有孩子结点的,这部分结点就称为叶子结点,那么也就是说,这部分结点是不需要向下调整的。我们选择从(length/2)-1的下标开始依次从0下标的方向进行调整。每次调整之后,调整的结点还要继续比较他的子结点看看是否仍然满足最大堆特点,一直调整到叶子结点。这样做的目的就是使数列的大值向上浮,小值向下沉。直到下标0结点(根结点)调整完成,此时就是一个最大堆。
此时根结点是一个最大值,我们把最大值排在无序数列最后,即把最大值与队尾交换位置。此时我们发现除了根结点,其他结点仍然是符合最大堆特点的(注意,从这个位置往后,我们讲述的情况都是排除了最后一个数,因为他已经排好了位置)。这时我们只用调整根结点就可以了,调整之后,就得到了数列的第二个最大值。依次调整,直到数列排好即可。
public static void HeapSort(int[] arr)
{
int length = arr.Length;
if (length < 2)
{
return;
}
//初次构建最大堆。从后往前第一个非叶子结点开始调整。
for (int i = length / 2 - 1; i >= 0; i--)
{
AdjustHeap(arr, i, length);
}
//将堆顶最大值移动到数组末端,再次从根结点开始调整构建最大堆。
//注意长度要-1,因为队尾的元素已经是排好序的。
for (int i = 0; i < length -1; i++)
{
Swap(arr, 0, length - 1 - i);
AdjustHeap(arr, 0, length - i - 1);
}
}
///
/// 构建最大堆
///
/// 需要构建的数组
/// 需要开始调整的结点下标
/// 需要构建的数组长度
private static void AdjustHeap(int[] arr, int index, int length)
{
int leftIndex = index * 2 + 1; //左孩子结点下标
int rightIndex = index * 2 + 2; //右孩子结点下标
//如果左孩子下标大于等于数组长,则说明其为叶子结点,不需要调整
if (leftIndex >= length)
{
return;
}
//找到左右结点最大值的下标
int maxIndex = leftIndex;
if (rightIndex < length)
{
if (arr[leftIndex] < arr[rightIndex])
{
maxIndex = rightIndex;
}
}
//如果孩子结点的值要大于父结点,则交换两个结点的值,
//并且从交换后的子结点继续向下调整
if (arr[maxIndex] > arr[index])
{
Swap(arr, maxIndex, index);
AdjustHeap(arr, maxIndex, length);
}
}
private static void Swap(int[] arr, int index1, int index2)
{
int length = arr.Length;
if (index1 >= length || index2 >= length)
{
return;
}
int temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。 若将两个有序表合并成一个有序表,称为二路归并。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
算法步骤
public static void MergeSort(int[] array1)
{
int low = 0, heigh = array1.Length - 1;
Mergesort(array1, low, heigh);
}
private static void Mergesort(int[] array1,int low,int heigh)
{
if (low < heigh)
{
int mid = (low+heigh) / 2;
Mergesort(array1,low,mid);
Mergesort(array1, mid + 1, heigh);
BinaryMerge(array1,low,mid,heigh);
}
}
public static void BinaryMerge(int[] array, int low,int mid,int height)
{
int[] temparray = new int[array.Length];
int left, right,index;
//复制数组
for (index = low; index <= height; index++)
{
temparray[index] = array[index];
}
//二路归并
for ( index= left = low,right=mid+1;left<=mid&& right <= height && index <=height; index++)
{
if (temparray[left] <= temparray[right]) { array[index] = temparray[left++]; }
else { array[index] = temparray[right++]; }
}
//检查那个部分没拷贝完成,将temparray剩余的部分拷贝到array数组中
while (left<=mid) array[index++] = temparray[left++];
while(right<=height) array[index++]=temparray[right++];
}
计数排序是一种适合于最大值和最小值的差值不是不是很大的排序。例如 temp[i] = m, 表示元素 i 一共出现了 m 次
static void CountSort(int[] arr)
{
//1.得到数列的最大值 和 最小值
int Max = arr[0];
int Min = arr[0];
for (int i = 1; i < arr.Length; i++)
{
if (arr[i] > Max) Max = arr[i];
if (arr[i] < Min) Min = arr[i];
}
//2.根据数列最大值确定统计数组的长度
int[] newArr = new int[Max -Min+ 1];
//3.遍历数列,填充统计数组 记得加上 偏移量Min
for (int i = 0; i < arr.Length; i++)
{
newArr[arr[i]-Min]++;
}
//4.遍历统计数组,输出结果 记得加上 偏移量Min
int Index = 0;
for (int i = 0; i < newArr.Length; i++)
{
for (int j = 0; j < newArr[i]; j++)
{
arr[Index++] = i+Min;
}
}
}
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
算法步骤
///
/// 桶排序
///
public class Program {
public static void Main(string[] args) {
double[] array = { 0.43, 0.69, 0.11, 0.72, 0.28, 0.21, 0.56, 0.80, 0.48, 0.94, 0.32, 0.08 };
BucketSort(array, 10);
ShowSord(array);
Console.ReadKey();
}
private static void ShowSord(double[] array) {
foreach (var num in array) {
Console.Write($"{num} ");
}
Console.WriteLine();
}
public static void BucketSort(double[] array, int bucketNum) {
//创建bucket时,在二维中增加一组标识位,其中bucket[x, 0]表示这一维所包含的数字的个数
//通过这样的技巧可以少写很多代码
double[,] bucket = new double[bucketNum, array.Length + 1];
foreach (var num in array) {
int bit = (int)(10 * num);
bucket[bit, (int)++bucket[bit, 0]] = num;
}
//为桶里的每一行使用插入排序
for (int j = 0; j < bucketNum; j++) {
//为桶里的行创建新的数组后使用插入排序
double[] insertion = new double[(int)bucket[j, 0]];
for (int k = 0; k < insertion.Length; k++) {
insertion[k] = bucket[j, k + 1];
}
//插入排序
StraightInsertionSort(insertion);
//把排好序的结果回写到桶里
for (int k = 0; k < insertion.Length; k++) {
bucket[j, k + 1] = insertion[k];
}
}
//将所有桶里的数据回写到原数组中
for (int count = 0, j = 0; j < bucketNum; j++) {
for (int k = 1; k <= bucket[j, 0]; k++) {
array[count++] = bucket[j, k];
}
}
}
public static void StraightInsertionSort(double[] array) {
//插入排序
for (int i = 1; i < array.Length; i++) {
double sentinel = array[i];
int j = i - 1;
while (j >= 0 && sentinel < array[j]) {
array[j + 1] = array[j];
j--;
}
array[j + 1] = sentinel;
}
}
}
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
基数相对于其他排序,算法逻辑和代码都相对简单。
代码方面,也都是和另外两个排序的顺序一致。
与另外两个非比较排序不同的是:
计数排序和桶排序是基于数组索引来的,所以只能排序整数
但是基于数的计数而定,所以在改造后,理论上是也可以排序非整数
算法步骤
注:正反填充需要进行循环,循环次数为步骤一的位数
public int[] RadixSort2(int[] array)
{
//求最(大)值
int max = array[0];
foreach (var item in array)
{
max = item > max ? item : max;
}
int maxDigit = 0;
while(max!=0)
{
max /= 10;maxDigit++;
}
//初新桶
var bucket = new List<List<int>>();
for (int i = 0; i < 10; i++)
{
bucket.Add(new List<int>());
}
for (int i = 0; i < maxDigit; i++)
{
//正填充
int div = (int)Math.Pow(10, (i + 1));
foreach (var item in array)
{
//获取基数
int radix = (item % div) / (div / 10);
bucket[radix].Add(item);
}
//反填充(//反填充要注意顺序)
int index = 0;
foreach (var item in bucket)
{
foreach (var it in item)
{
array[index++] = it;
}
item.Clear();//清除数据
}
}
return array;
}
C#十大排序算法
C#快速排序详解
排序算法2|简单选择排序与堆排序(C#)
【C# 排序】归并排序 merge sort
C#计数排序
【愚公系列】2021年11月 C#版 数据结构与算法解析(桶排序)
C# 算法之基数排序排序(非比较排序之三)