内排序:在排序过程中,所有元素调到内存中进行的排序,称为内排序。内排序是排序的基础。内排序效率用比较次数来衡量。
外排序:在数据量大的情况下,只能分块排序,但块与块间不能保证有序。外排序用读/写外存的次数来衡量其效率。
快速排序是冒泡排序的改进版,也是最好的一种内排序方法。
原理(分治思想):
1.在待排序的元素任取一个元素作为基准(通常选第一个元素),称为基准元素;
2.将待排序的元素进行分区,比基准元素大的元素放在它的右边,比其小的放在它的左边;
3.对左右两个分区重复以上步骤直到所有元素都是有序的。
本质上,每一轮快速排序能够令一个基准元素落位。基本实现思路有三种,分别为挖坑填数法、左右(首尾)指针法、追赶(前后)指针法。
挖坑填数法:
1、设置一个临时变量,用来存储每一轮排序的基准元素。相当于将基准元素挖出,该位置上可将其他元素填入。
如图,令 X=72 ,相当于挖去数组首元素,可填入其他数字(红色位置代表坑)
72 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 48 | 85 |
2、先从右向左扫描数组,依次与基准元素做比较,直至找到一个小于基准元素的数,将其赋值给上个步骤空出的位置。相当于挖出小数将其填入上一个坑,但同时产生了另一个坑。
将右侧较小元素挖出,填入前面的坑
48 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 48 | 85 |
3、再从左向右扫描数组(刨除基准元素),依次与基准元素做比较,直至找到一个大于等于基准元素的数,将其赋值给上个步骤空出的位置。相当于挖出大数将其填入上一个坑,但同时产生了另一个坑。
将右侧较大元素挖出,填入后面的坑
48 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 88 | 85 |
4、重复2、3直至左右扫描过程扫描到数组同一个位置,该位置必为坑位,将基准元素填入即可。此时比基准元素小的数都在左边,大于等于基准元素的数都在右边。
最终扫描过程终止在42处,此时左边的数都小于72,右边的数都大于72
48 | 6 | 57 | 42 | 60 | 42 | 83 | 73 | 88 | 85 |
将最初挖出的基准元素填入这个坑即完成第一轮排序
48 | 6 | 57 | 42 | 60 | 72 | 83 | 73 | 88 | 85 |
5、递归的对基准元素左右两边做快速排序,直至数组有序。
注意:之所以要先从右侧开始扫描,是因为我们想要使数组从小到大排列,所以必须保证左侧经过扫描的数必须小于基准元素,右边经过扫描的数必须大于基准元素。而基础元素被挖出留下的坑在数组左侧,必须填入小于基准元素的数,因此只有先从右侧进行扫描才可以忽略大于基准元素的数去寻找小于基准元素的数。
代码:
void quicksort(int A[], int l, int r) //l、r表示快速排序的起始和终止元素下标。
{
if (l < r)
{
int i = l, j = r, pivot = A[l];
while (i < j)
{
while(i < j && A[j] >= pivot) // 从右向左找第一个小于pivot的数
j--;
if(i < j)
A[i++] = A[j];
while(i < j && A[i] < pivot) // 从左向右找第一个大于等于pivot的数
i++;
if(i < j)
A[j--] = A[i];
}
A[i] = pivot; //跳出循环说明 i==j,此时将基准数填入坑中
quicksort(A, l, i - 1); // 递归调用
quicksort(A, i + 1, r);
}
}
左右指针法:
1、选定第一个元素为基准数。
红色为基准元素
2、先从右向左扫描数组,依次与基准数进行比较,找出小于基准数的元素。
蓝色为小于基准元素的数
3、再从左向右扫描数组,依次与基准数进行比较,找出大于等于基准数的元素。
绿色为大于等于基准元素的数
4、然后将2、3中找出的两个数进行交换。
5、重复2、3、4步骤直至左右扫描过程扫描到数组同一位置,该位置上的元素必小于基准元素,将基准元素与其交换位置即可。此时比基准元素小的数都在左边,大于等于基准元素的数都在右边。
黄色表示左右指针相遇
将基准元素与其互换
6、递归的对基准元素左右两边做快速排序,直至数组有序。
代码:
void quicksort(int A[], int l, int r) //l、r表示快速排序的起始和终止元素下标。
{
if (l < r)
{
int i = l + 1, j = r, pivot = A[l], temp; //i,j分别为左、右指针指向的元素下标;temp是用作元素交换的临时变量
while (i < j)
{
while (i < j && A[j] >= pivot) // 从右向左找第一个小于pivot的数
j--;
while (i < j && A[i] < pivot) // 从左向右找第一个大于等于pivot的数
i++;
if (i < j)
{
temp = A[i];
A[i] = A[j];
A[j--] = temp;
}
if (i<j)
i++;
}
if (A[l] > A[i]) //跳出循环说明 i==j,此时将基准元素与其进行互换
{
A[i] = A[l] + A[i];
A[l] = A[i] - A[l];
A[i] = A[i] - A[l];
}
quicksort(A, l, i - 1); // 递归调用
quicksort(A, i + 1, r);
}
}
追赶指针法:
1、选定第一个元素为基准元素。
红色为基准元素
2、前指针从第二个元素开始,后指针从第一个元素开始。前者寻找小于基准元素的数,后者寻找大于等于基准元素的数。前指针先出发,如果遇到小于基准元素的数则停止,否则一直前进。后指针每当前指针停止时前进一步,如果遇到大于等于基准元素的数则与前指针交换,再令前指针继续前进;否则直接令前指针继续前进(易知,只有前指针找到小于基准元素的数之前遇到过大于等于基准元素的数才会与后指针拉开差距,否则后指针必将紧跟着前指针)。实际上,后指针不用费力寻找大于等于基准元素的数,因为前指针探测过而又没有与后指针发生交换的数必定大于等于基准元素,亦即前后指针之间的元素必大于等于基准数。因此只需等待前指针找到小于基准元素的数时,后指针自增后与其进行交换即可。
蓝色箭头表示前指针,绿色箭头表示后指针,起始时如图
前指针先出发,直至遇到小于基准元素的数(蓝色),则停止
后指针每当前指针停止时前进一步
前指针继续行动,仍然遇到小于基准元素的数停止,并令后指针前进一步
前指针再次行动,跳过大于等于基准数的元素,停止于小于基准数的元素
后指针前进一步并与前指针进行交换
3、重复步骤2,直至前指针遍历完最后一个元素。后指针指向的元素必定小于基准数,将其与基准元素交换即完成第一轮排序。此时基准元素左边的数均小于基准数,右边的数均大于等于基准数。
前指针遍历至最后一个元素
将后指针指向的元素与基准元素进行交换
5、递归的对基准元素左右两边做快速排序,直至数组有序。
代码:
void quicksort(int A[], int l, int r) //l、r表示快速排序的起始和终止元素下标。
{
if (l < r)
{
int i = l, j=l+1, pivot = A[l], temp; //i,j分别为后、前指针指向的元素下标,初始时相邻;temp是用于元素交换的临时变量
while (j <= r)
{
if (A[j]<pivot && ++i!=j) //条件1若不为真则不再验证条件2,直接令j自增(即前指针继续前进)。若j指向的元素小于基准元素,则令i自增(即令后指针前进),并判断i、j是否相邻(只有j前进时说明遇到了大于基准元素的数,将与i拉开距离,也可以说前后指针直接的数必大于基准元素)
{
temp = A[i];
A[i] = A[j];
A[j] = temp;
}
j++;
}
if (l != i)
{
A[i] = A[l] + A[i]; //跳出循环说明 j>r 前指针已经越界,此时将基准元素与第i个元素进行互换(由于最后一次交换之后,后指针指向的元素必定小于基准元素,后面的元素均大于基准元素)
A[l] = A[i] - A[l];
A[i] = A[i] - A[l];
}
quicksort(A, l, i - 1); // 递归调用
quicksort(A, i + 1 , r);
}
}
快速排序的优化
快速排序为冒泡排序的改进,本质同为交换排序,在基准元素为最值的情况下将退化为冒泡排序。因此对于已排序程度较高的序列,其效率将大大降低。为了解决这个问题,我们可以利用以下几种思路来优化快速排序:
1、三数取中
取基准数时,从首尾元素以及中间元素之中取中位数做为基准数。
2、随机选取基准数
3、子序列小于一定规模或者部分有序时,改用插入排序(通常处理规模小于10的序列)。
4、把与基准元素相等的数聚集到中间,分割子序列时不再考虑它们
每轮排序完成后,分割子序列时,先将与基准数相同的元素放置到数组两端并计数,然后再将它们移至基准元素两边。对子序列进行快排时可排除这些元素。
参考文章:https://blog.csdn.net/qq_36528114/article/details/78667034