算法就好比程序开发中的利剑,所到之处,刀起头落。
针对现实中的排序问题,算法有七把利剑可以助你马道成功。
首先排序分为四种:
交换排序: 包括冒泡排序,快速排序。
选择排序: 包括直接选择排序,堆排序。
插入排序: 包括直接插入排序,希尔排序。
合并排序: 合并排序。
那么今天我们讲的就是交换排序,我们都知道,C#类库提供的排序是快排,为了让今天玩的有意思点,
我们设计算法来跟类库提供的快排较量较量。争取KO对手。
冒泡排序:
首先我们自己来设计一下“冒泡排序”,这种排序很现实的例子就是:
我抓一把沙仍进水里,那么沙子会立马沉入水底, 沙子上的灰尘会因为惯性暂时沉入水底,但是又会立马像气泡一样浮出水面,最后也就真相大白咯。
关于冒泡的思想,我不会说那么官方的理论,也不会贴那些文字上来,我的思想就是看图说话。
那么我们就上图.
要达到冒泡的效果,我们就要把一组数字竖起来看,大家想想,如何冒泡?如何来体会重的沉底,轻的上浮?
第一步: 我们拿40跟20比,发现40是老大,不用交换。
第二步: 然后向前推一步,就是拿20跟30比,发现30是老大,就要交换了。
第三步:拿交换后的20跟10比,发现自己是老大,不用交换。
第四步:拿10跟50交换,发现50是老大,进行交换。
最后,我们经过一次遍历,把数组中最小的数字送上去了,看看,我们向目标又迈进了一步。
现在大家思想都知道了,下面我们就强烈要求跟快排较量一下,不是你死就是我活。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using System.Threading; namespace BubbleSort { public class Program { static void Main(string[] args) { //五次比较 for (int i = 1; i <= 5; i++) { List<int> list = new List<int>(); //插入2k个随机数到数组中 for (int j = 0; j < 2000; j++) { Thread.Sleep(1); list.Add(new Random((int)DateTime.Now.Ticks).Next(0, 100000)); } Console.WriteLine("\n第" + i + "次比较:"); Stopwatch watch = new Stopwatch(); watch.Start(); var result = list.OrderBy(single => single).ToList(); watch.Stop(); Console.WriteLine("\n快速排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前是十个数:" + string.Join(",", result.Take(10).ToList())); watch.Start(); result = BubbleSort(list); watch.Stop(); Console.WriteLine("\n冒泡排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前是十个数:" + string.Join(",", result.Take(10).ToList())); } } //冒泡排序算法 static List<int> BubbleSort(List<int> list) { int temp; //第一层循环: 表明要比较的次数,比如list.count个数,肯定要比较count-1次 for (int i = 0; i < list.Count - 1; i++) { //list.count-1:取数据最后一个数下标, //j>i: 从后往前的的下标一定大于从前往后的下标,否则就超越了。 for (int j = list.Count - 1; j > i; j--) { //如果前面一个数大于后面一个数则交换 if (list[j - 1] > list[j]) { temp = list[j - 1]; list[j - 1] = list[j]; list[j] = temp; } } } return list; } } }
呜呜,看着这两种排序体检报告,心都凉了,冒泡被快排KO了,真惨,难怪人家说冒泡效率低,原来真tmd低。
快速排序:
既然能把冒泡KO掉,马上就激起我们的兴趣,tnd快排咋这么快,一定要好好研究一下。
从图中我们可以看到:
left指针,right指针,base参照数。
其实思想是蛮简单的,就是通过第一遍的遍历(让left和right指针重合)来找到数组的切割点。
第一步:首先我们从数组的left位置取出该数(20)作为基准(base)参照物。
第二步:从数组的right位置向前找,一直找到比(base)小的数,
如果找到,将此数赋给left位置(也就是将10赋给20),
此时数组为:10,40,50,10,60,
left和right指针分别为前后的10。
第三步:从数组的left位置向后找,一直找到比(base)大的数,
如果找到,将此数赋给right的位置(也就是40赋给10),
此时数组为:10,40,50,40,60,
left和right指针分别为前后的40。
第四步:重复“第二,第三“步骤,直到left和right指针重合,
最后将(base)插入到40的位置,
此时数组值为: 10,20,50,40,60,至此完成一次排序。
第五步:此时20已经潜入到数组的内部,20的左侧一组数都比20小,20的右侧作为一组数都比20大,
以20为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。
同样,我们把自己设计的快排跟类库提供的快拍比较一下。看谁牛X。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Diagnostics; namespace QuickSort { public class Program { static void Main(string[] args) { //5次比较 for (int i = 1; i <= 5; i++) { List<int> list = new List<int>(); //插入200个随机数到数组中 for (int j = 0; j < 200; j++) { Thread.Sleep(1); list.Add(new Random((int)DateTime.Now.Ticks).Next(0, 10000)); } Console.WriteLine("\n第" + i + "次比较:"); Stopwatch watch = new Stopwatch(); watch.Start(); var result = list.OrderBy(single => single).ToList(); watch.Stop(); Console.WriteLine("\n系统定义的快速排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前是十个数:" + string.Join(",", result.Take(10).ToList())); watch.Start(); new QuickSortClass().QuickSort(list, 0, list.Count - 1); watch.Stop(); Console.WriteLine("\n俺自己写的快速排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前是十个数:" + string.Join(",", list.Take(10).ToList())); } } } public class QuickSortClass { ///<summary> /// 分割函数 ///</summary> ///<param name="list">待排序的数组</param> ///<param name="left">数组的左下标</param> ///<param name="right"></param> ///<returns></returns> public int Division(List<int> list, int left, int right) { //首先挑选一个基准元素 int baseNum = list[left]; while (left < right) { //从数组的右端开始向前找,一直找到比base小的数字为止(包括base同等数) while (left < right && list[right] >= baseNum) right = right - 1; //最终找到了比baseNum小的元素,要做的事情就是此元素放到base的位置 list[left] = list[right]; //从数组的左端开始向后找,一直找到比base大的数字为止(包括base同等数) while (left < right && list[left] <= baseNum) left = left + 1; //最终找到了比baseNum大的元素,要做的事情就是将此元素放到最后的位置 list[right] = list[left]; } //最后就是把baseNum放到该left的位置 list[left] = baseNum; //最终,我们发现left位置的左侧数值部分比left小,left位置右侧数值比left大 //至此,我们完成了第一篇排序 return left; } public void QuickSort(List<int> list, int left, int right) { //左下标一定小于右下标,否则就超越了 if (left < right) { //对数组进行分割,取出下次分割的基准标号 int i = Division(list, left, right); //对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, left, i - 1); //对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序 QuickSort(list, i + 1, right); } } } }
不错,快排就是快,难怪内库非要用他来作为排序的标准。
嗯,最后要分享下:
冒泡的时间复杂度为: 0(n) - 0(n^2)
快排的时间复杂度为:
平均复杂度: N(logN)
最坏复杂度: 0(n^2)
选择排序,包括“直接选择排序”和“堆排序”。
话说上次“冒泡排序”被快排虐了,而且“快排”赢得了内库的重用,众兄弟自然眼红,非要找快排一比高下。
这不今天就来了两兄弟找快排算账。
1.直接选择排序:
先上图:
说实话,直接选择排序最类似于人的本能思想,比如把大小不一的玩具让三岁小毛孩对大小排个序,
那小孩首先会在这么多玩具中找到最小的放在第一位,然后找到次小的放在第二位,以此类推。。。。。。
,小孩子多聪明啊,这么小就知道了直接选择排序。羡慕中........
对的,小孩子给我们上了一课,
第一步: 我们拿80作为参照物(base),在80后面找到一个最小数20,然后将80跟20交换。
第二步: 第一位数已经是最小数字了,然后我们推进一步在30后面找一位最小数,发现自己最小,不用交换。
第三步:........
最后我们排序完毕。大功告成。
既然是来挑战的,那就5局3胜制。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Diagnostics; namespace SelectionSort { public class Program { static void Main(string[] args) { //5次比较 for (int i = 1; i <= 5; i++) { List<int> list = new List<int>(); //插入2w个随机数到数组中 for (int j = 0; j < 20000; j++) { Thread.Sleep(1); list.Add(new Random((int)DateTime.Now.Ticks).Next(1000, 1000000)); } Console.WriteLine("\n第" + i + "次比较:"); Stopwatch watch = new Stopwatch(); watch.Start(); var result = list.OrderBy(single => single).ToList(); watch.Stop(); Console.WriteLine("\n快速排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前十个数:" + string.Join(",", result.Take(10).ToList())); watch.Start(); result = SelectionSort(list); watch.Stop(); Console.WriteLine("\n直接选择排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前十个数:" + string.Join(",", list.Take(10).ToList())); } } //选择排序 static List<int> SelectionSort(List<int> list) { //要遍历的次数 for (int i = 0; i < list.Count - 1; i++) { //假设tempIndex的下标的值最小 int tempIndex = i; for (int j = i + 1; j < list.Count; j++) { //如果tempIndex下标的值大于j下标的值,则记录较小值下标j if (list[tempIndex] > list[j]) tempIndex = j; } //最后将假想最小值跟真的最小值进行交换 var tempData = list[tempIndex]; list[tempIndex] = list[i]; list[i] = tempData; } return list; } } }
比赛结果公布:
堆排序:
要知道堆排序,首先要了解一下二叉树的模型。
下图就是一颗二叉树,具体的情况我后续会分享的。
那么堆排序中有两种情况(看上图理解):
大根堆: 就是说父节点要比左右孩子都要大。
小根堆: 就是说父节点要比左右孩子都要小。
那么要实现堆排序,必须要做两件事情:
第一:构建大根堆。
首先上图:
首先这是一个无序的堆,那么我们怎样才能构建大根堆呢?
第一步: 首先我们发现,这个堆中有2个父节点(2,,3);
第二步: 比较2这个父节点的两个孩子(4,5),发现5大。
第三步: 然后将较大的右孩子(5)跟父节点(2)进行交换,至此3的左孩子堆构建完毕,
如图:
第四步: 比较第二个父节点(3)下面的左右孩子(5,1),发现左孩子5大。
第五步: 然后父节点(3)与左孩子(5)进行交换,注意,交换后,堆可能会遭到破坏,
必须按照以上的步骤一,步骤二,步骤三进行重新构造堆。
最后构造的堆如下:
第二:输出大根堆。
至此,我们把大根堆构造出来了,那怎么输出呢?我们做大根堆的目的就是要找出最大值,
那么我们将堆顶(5)与堆尾(2)进行交换,然后将(5)剔除根堆,由于堆顶现在是(2),
所以破坏了根堆,必须重新构造,构造完之后又会出现最大值,再次交换和剔除,最后也就是俺们
发现自己兄弟被别人狂殴,,堆排序再也坐不住了,决定要和快排干一场。
同样,快排也不甘示弱,谁怕谁?
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Diagnostics; namespace HeapSort { public class Program { static void Main(string[] args) { //5次比较 for (int j = 1; j <= 5; j++) { List<int> list = new List<int>(); //插入2w个数字 for (int i = 0; i < 20000; i++) { Thread.Sleep(1); list.Add(new Random((int)DateTime.Now.Ticks).Next(1000, 100000)); } Console.WriteLine("\n第" + j + "次比较:"); Stopwatch watch = new Stopwatch(); watch.Start(); var result = list.OrderBy(single => single).ToList(); watch.Stop(); Console.WriteLine("\n快速排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前十个数" + string.Join(",", result.Take(10).ToList())); watch = new Stopwatch(); watch.Start(); HeapSort(list); watch.Stop(); Console.WriteLine("\n堆排序耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前十个数" + string.Join(",", list.Take(10).ToList())); } } ///<summary> /// 构建堆 ///</summary> ///<param name="list">待排序的集合</param> ///<param name="parent">父节点</param> ///<param name="length">输出根堆时剔除最大值使用</param> static void HeapAdjust(List<int> list, int parent, int length) { //temp保存当前父节点 int temp = list[parent]; //得到左孩子(这可是二叉树的定义,大家看图也可知道) int child = 2 * parent + 1; while (child < length) { //如果parent有右孩子,则要判断左孩子是否小于右孩子 if (child + 1 < length && list[child] < list[child + 1]) child++; //父亲节点大于子节点,就不用做交换 if (temp >= list[child]) break; //将较大子节点的值赋给父亲节点 list[parent] = list[child]; //然后将子节点做为父亲节点,已防止是否破坏根堆时重新构造 parent = child; //找到该父亲节点较小的左孩子节点 child = 2 * parent + 1; } //最后将temp值赋给较大的子节点,以形成两值交换 list[parent] = temp; } ///<summary> /// 堆排序 ///</summary> ///<param name="list"></param> public static void HeapSort(List<int> list) { //list.Count/2-1:就是堆中父节点的个数 for (int i = list.Count / 2 - 1; i >= 0; i--) { HeapAdjust(list, i, list.Count); } //最后输出堆元素 for (int i = list.Count - 1; i > 0; i--) { //堆顶与当前堆的第i个元素进行值对调 int temp = list[0]; list[0] = list[i]; list[i] = temp; //因为两值交换,可能破坏根堆,所以必须重新构造 HeapAdjust(list, 0, i); } } } }
结果公布:
堆排序此时心里很尴尬,双双被KO,心里想,一定要捞回面子,一定要赢,
于是堆排序提出了求“前K大问题”。(就是在海量数据中找出前几大的数据),
快排一口答应,小意思,没问题。
双方商定,在2w随机数中找出前10大的数:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Diagnostics; namespace QuickSort { public class Program { static void Main(string[] args) { //5此比较 for (int j = 1; j <= 5; j++) { List<int> list = new List<int>(); for (int i = 0; i < 20000; i++) { Thread.Sleep(1); list.Add(new Random((int)DateTime.Now.Ticks).Next(1000, 100000)); } Console.WriteLine("\n第" + j + "次比较:"); Stopwatch watch = new Stopwatch(); watch.Start(); var result = list.OrderByDescending(single => single).Take(10).ToList(); watch.Stop(); Console.WriteLine("\n快速排序求前K大耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前十个数:" + string.Join(",", result.Take(10).ToList())); watch = new Stopwatch(); watch.Start(); result = HeapSort(list, 10); watch.Stop(); Console.WriteLine("\n堆排序求前K大耗费时间:" + watch.ElapsedMilliseconds); Console.WriteLine("输出前十个数:" + string.Join(",", list.Take(10).ToList())); } } ///<summary> /// 构建堆 ///</summary> ///<param name="list">待排序的集合</param> ///<param name="parent">父节点</param> ///<param name="length">输出根堆时剔除最大值使用</param> static void HeapAdjust(List<int> list, int parent, int length) { //temp保存当前父节点 int temp = list[parent]; //得到左孩子(这可是二叉树的定义哇) int child = 2 * parent + 1; while (child < length) { //如果parent有右孩子,则要判断左孩子是否小于右孩子 if (child + 1 < length && list[child] < list[child + 1]) child++; //父节点大于子节点,不用做交换 if (temp >= list[child]) break; //将较大子节点的值赋给父亲节点 list[parent] = list[child]; //然后将子节点做为父亲节点,已防止是否破坏根堆时重新构造 parent = child; //找到该父节点左孩子节点 child = 2 * parent + 1; } //最后将temp值赋给较大的子节点,以形成两值交换 list[parent] = temp; } ///<summary> /// 堆排序 ///</summary> ///<param name="list">待排序的集合</param> ///<param name="top">前K大</param> ///<returns></returns> public static List<int> HeapSort(List<int> list, int top) { List<int> topNode = new List<int>(); //list.Count/2-1:就是堆中非叶子节点的个数 for (int i = list.Count / 2 - 1; i >= 0; i--) { HeapAdjust(list, i, list.Count); } //最后输出堆元素(求前K大) for (int i = list.Count - 1; i >= list.Count - top; i--) { //堆顶与当前堆的第i个元素进行值对调 int temp = list[0]; list[0] = list[i]; list[i] = temp; //最大值加入集合 topNode.Add(temp); //因为顺序被打乱,必须重新构造堆 HeapAdjust(list, 0, i); } return topNode; } } }
求前K大的输出结果:
最后堆排序赶紧拉着直接选择排序一路小跑了,因为求前K大问题已经不是他原本来的目的。
ps: 直接选择排序的时间复杂度为:O(n^2)
堆排序的时间复杂度:O(NlogN)
直接插入排序:
这种排序其实蛮好理解的,很现实的例子就是俺们斗地主,当我们抓到一手乱牌时,我们就要按照大小梳理扑克,30秒后,
扑克梳理完毕,4条3,5条s,哇塞...... 回忆一下,俺们当时是怎么梳理的。
最左一张牌是3,第二张牌是5,第三张牌又是3,赶紧插到第一张牌后面去,第四张牌又是3,大喜,赶紧插到第二张后面去,
第五张牌又是3,狂喜,哈哈,一门炮就这样产生了。
怎么样,生活中处处都是算法,早已经融入我们的生活和血液。
下面就上图说明:
看这张图不知道大家可否理解了,在插入排序中,数组会被划分为两种,“有序数组块”和“无序数组块”,
对的,第一遍的时候从”无序数组块“中提取一个数20作为有序数组块。
第二遍的时候从”无序数组块“中提取一个数60有序的放到”有序数组块中“,也就是20,60。
第三遍的时候同理,不同的是发现10比有序数组的值都小,因此20,60位置后移,腾出一个位置让10插入。
然后按照这种规律就可以全部插入完毕。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace InsertSort { public class Program { static void Main(string[] args) { List<int> list = new List<int>() { 3, 1, 2, 9, 7, 8, 6 }; Console.WriteLine("排序前:" + string.Join(",", list)); InsertSort(list); Console.WriteLine("排序后:" + string.Join(",", list)); } static void InsertSort(List<int> list) { //无须序列 for (int i = 1; i < list.Count; i++) { var temp = list[i]; int j; //有序序列 for (j = i - 1; j >= 0 && temp < list[j]; j--) { list[j + 1] = list[j]; } list[j + 1] = temp; } } } }
看的出来,希尔排序优化了不少,w级别的排序中,相差70几倍哇。
归并排序:
个人感觉,我们能容易看的懂的排序基本上都是O (n^2),比较难看懂的基本上都是N(LogN),所以归并排序也是比较难理解的,尤其是在代码
编写上,本人就是搞了一下午才搞出来,嘻嘻。
归并排序中中两件事情要做:
第一: “分”, 就是将数组尽可能的分,一直分到原子级别。
第二: “并”,将原子级别的数两两合并排序,最后产生结果。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MergeSort { class Program { static void Main(string[] args) { int[] array = { 3, 2, 1, 8, 9, 0 }; MergeSort(array, new int[array.Length], 0, array.Length - 1); Console.WriteLine(string.Join(",", array)); } ///<summary> /// 数组的划分 ///</summary> ///<param name="array">待排序数组</param> ///<param name="temparray">临时存放数组</param> ///<param name="left">序列段的开始位置,</param> ///<param name="right">序列段的结束位置</param> static void MergeSort(int[] array, int[] temparray, int left, int right) { if (left < right) { //取分割位置 int middle = (left + right) / 2; //递归划分数组左序列 MergeSort(array, temparray, left, middle); //递归划分数组右序列 MergeSort(array, temparray, middle + 1, right); //数组合并操作 Merge(array, temparray, left, middle + 1, right); } } ///<summary> /// 数组的两两合并操作 ///</summary> ///<param name="array">待排序数组</param> ///<param name="temparray">临时数组</param> ///<param name="left">第一个区间段开始位置</param> ///<param name="middle">第二个区间的开始位置</param> ///<param name="right">第二个区间段结束位置</param> static void Merge(int[] array, int[] temparray, int left, int middle, int right) { //左指针尾 int leftEnd = middle - 1; //右指针头 int rightStart = middle; //临时数组的下标 int tempIndex = left; //数组合并后的length长度 int tempLength = right - left + 1; //先循环两个区间段都没有结束的情况 while ((left <= leftEnd) && (rightStart <= right)) { //如果发现有序列大,则将此数放入临时数组 if (array[left] < array[rightStart]) temparray[tempIndex++] = array[left++]; else temparray[tempIndex++] = array[rightStart++]; } //判断左序列是否结束 while (left <= leftEnd) temparray[tempIndex++] = array[left++]; //判断右序列是否结束 while (rightStart <= right) temparray[tempIndex++] = array[rightStart++]; //交换数据 for (int i = 0; i < tempLength; i++) { array[right] = temparray[right]; right--; } } } }
ps: 插入排序的时间复杂度为:O(N^2)
希尔排序的时间复杂度为:平均为:O(N^3/2)
最坏: O(N^2)
归并排序时间复杂度为: O(NlogN)
空间复杂度为: O(N)
感谢博主的详细讲解,受用中!