快速排序
快排和归并排序是比较经常见到的两种排序算法,下面我将用我自己的话来讲解快速排序和归并的知识点。
(一)快速排序
首先要明白,快速排序是基于冒泡排序的基础上做得改进,冒泡排序的交换次数太多了,快速排序就是为了来减少交换次数的,那接下来就好好的分析一下整个过程。
快速排序定义:快排是利用分治的思想,如果要排序的数组中下标是从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为分区点(pivot(分区点)),我们遍历p到r之间的数据,将小于pivot放到左边,将大于pivot放到右边,将pivot放到中间,这样经过这一步骤之后,数组p到r之间的数据分成了三部分。前面p到q-1都是小于pivot的,中间pivot, 后面的q+1到r之间都是大于pivot的。
根据分治的思想,我们可以想到用递归来实现快速排序;
递归的跳出条件就是:当p和r相等的时候,待排序的数组里面只剩下一个元素了。
递推公式就是:f(x)=f(p...q-1)+f(q+1,r) 。
大概的递归伪代码如下
quickSort(arr,p,r) {
if(p<r)
{ q=partion(arr,p,r) quickSort(arr,p,q-1) quickSort(arr,q+1,r)
} }
接下来最关键的就是这分区函数的实现;
分区函数的实现,有很多种方式。下面这个是我认为最好理解的。
我们先选取最右边的作为基准参数,这个i和j的作用大概如下:把数组arr分割成[p到i-1]是小于pivot,类似于选择排序。我们假定arr[i]代码的就是小于基准的数据。
static int Partion(ref List<int> intList,int l,int r) { var arr = intList.ToArray(); var pivot = arr[r]; int j = l; int i = 0; // for(j = l; j <=r; j++) { if (arr[j] <= pivot) { var temp = arr[j]; arr[j] = arr[i]; arr[i] = temp; if(arr[j]!=pivot) i++; } } intList = arr.ToList(); return i; }
(二)时间复杂度和空间复杂度分析
快速排序是原地,不稳定排序算法。原地排序算法,上面已经做到了。
空间复杂度为o(1)。不稳定的排序算法例如:6,8,7,6,4,9,5 分区完了之后,两个6的前后顺序会改变。
那么时间复杂度怎么分析:
这里主要是用到了递归,和第一次分区的时间复杂度为o(n);
最好的情况下,我们刚刚好平均分成了两份,如果我们对n个元素进行快速排序所需要的时间T(n),那么时间复杂度的公式如下:
T(1)=1;
T(n)=2(T/n)+n;
通过这个公式,如何来求解T(n)呢,还不够直观,那我们再进一步分解计算过程。
T(n)=2*T(n/2)+n
=2*(2*T(n/4)+n/2)+N=4*T(n/4)+2*n
=4*(2*T(n/8)+n/4)+2*n
=以此类推... 得到时间复杂度为o(nlogn)。
最坏情况下,就是o(n²)。
思考题:为什么快速排序需要定义两个变量(i和j)
哪里都有讲快速排序的,啥的,我也看了很多篇别人写的文章。但是好多文章里面都是直接就说拿两个i,j变量,就开始比较了。
快速排序,简单的说就是,先确定一个基准,然后就把比这个基准大的放到一边,比这个基准小的放到一边。放到一边的时候,是不管它是有序还是无序的。接下来在把之前的操作重复一下,其实就是一个递归的过程。
为什么要定义两个变量进行快速排序呢?
其实是这样的,快速排序,为啥叫快速排序呢,快在哪里,快在它减少了交换次数,还有一个原因就是快速排序是在快速排序是不用新开内存的,可以减少内存的使用。它就是在原地完成排序的。可能这样讲,你还是不明白,那我们来进行假设,如果只定义一个变量i的话呢,然后进行比较大小,那不就成了冒泡排序了吗。刚刚还说了快速排序是在原地完成排序的,不需要另外开辟空间资源的。你单独定义一个变量显然是不能实现的。
为啥要定义如果基准数量在这边,那么就要先从另外一边开始扫描呢。
先想想如果是放在同一边的话,那不就成了个冒泡排序吗。