在大批量的刷题之前,做好前期的准备工作,首先明白了时间复杂度和空间复杂度的计算方法,这个在我的上一篇博文里有提到,然后对经典排序算法做一个全面了解,2017.7.13,本文大部分内容引自http://www.cnblogs.com/eniac12/p/5329396.html 在自己不理解的地方着重重写了一下,代码实现上重新采用自己熟悉的语言java来实现,在正式开始前,需要掌握以下三个概念:
排序算法概述:我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。排序算法大体可分为两种:一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。
排序算法的稳定性:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。
1,对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性,。
2,而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。
3,需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。
排序算法稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。例如:这么说吧,一个班的学生已经按照学号大小排好序了,我现在要求按照年龄从小到大再排个序,如果年龄相同的,必须按照学号从小到大的顺序排列。那么问题来了,你选择的年龄排序方法如果是不稳定的,是不是排序完了后年龄相同的一组学生学号就乱了,你就得把这组年龄相同的学生再按照学号拍一遍。如果是稳定的排序算法,我就只需要按照年龄排一遍就好了。
本篇重点阐述比较排序的几种:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序。
##冒泡排序
###原理
冒泡排序是一种极其简单的排序算法,也是我所学的第一个排序算法。它重复地走访过要排序的元素,一次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。
###具体操作
- 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
###代码实现
package 比较排序;
import sun.security.util.Length;
/*
* 分类 -------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- O(n^2)
* 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
* 平均时间复杂度 ---- O(n^2)
* 所需辅助空间 ------ O(1)
* 稳定性 ------------ 稳定
*/
public class Comparesort {
public static void main(String[] args) {
int[] array = { 6, 5, 3, 1, 8, 7, 2, 4 };
new Comparesort().ComparesortVoid(array);
}
public void ComparesortVoid(int[] array){
int len =array.length;
System.out.println(len);
for (int i = 0; i < len-1; i++) {
// 每次最大元素就像气泡一样"浮"到数组的最后
for (int j = 0; j < len-i-1; j++) {
// 依次比较相邻的两个元素,使较大的那个向后移
if (array[j] > array[j+ 1]) //=================如果条件改成array[j] >= array[j + 1],则变为不稳定的排序算法
{
exchange(array, j, j + 1);
//每一趟i的比较都会沉底一个最大值,这个值在下一趟不必再比较,所以第i次外层循环会导致比第一次少i比较
}
}
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
}
}
public static void exchange(int[] array,int i,int j){ //交换位置函数
int temp;
temp = array[i];
array[i]=array[j];
array[j]=temp;
}
}
尽管冒泡排序是最容易了解和实现的排序算法之一,但它对于少数元素之外的数列排序是很没有效率的。
算法复杂度:(n-1)+(n-2)+…(n-n)=n*n-(1+2+3+…n) =n^2-(n+1)n/2 =1/2(n^2-n) = O(n^2)
###冒泡排序的改进:鸡尾酒排序原理
鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。
package 比较排序;
/*
* 分类 -------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- O(n^2)
* 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
* 平均时间复杂度 ---- O(n^2)
* 所需辅助空间 ------ O(1)
* 稳定性 ------------ 稳定
*/
public class Cocktailsort { // 鸡尾酒排序:冒泡排序的变体
public static void main(String[] args) {
int[] array = { 6, 5, 3, 1, 8, 7, 2, 4 };
new Cocktailsort().CocktailsortVoid(array);
;
}
public void CocktailsortVoid(int[] array) {
int len = array.length;
int left = 0;
int right = len - 1;
while (left < right) {
for (int i = left; i < right; i++) {
if (array[i] > array[i + 1]) {
exchange(array, i, i + 1); // 前半轮,将最大元素放到后面
}
}
right--;
for (int i = right; i > left; i--) {
if (array[i - 1] > array[i]) {
exchange(array, i - 1, i); //后半轮,将最小元素放到前面
}
}
left++;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
}
}
public static void exchange(int[] array, int i, int j) { // 交换位置函数
int temp;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
时间复杂度:n/2 * (n/2 +n/2)=n^2 /2 为O(n^2)
##选择排序
###原理
选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
###代码实现
package 比较排序;
/*
* 分类 -------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- O(n^2)
* 最优时间复杂度 ---- O(n^2)
* 平均时间复杂度 ---- O(n^2)
* 所需辅助空间 ------ O(1)
* 稳定性 ------------ 不稳定
*/
public class SelectionSort { //选择排序
public static void main(String[] args) {
int array[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 从小到大选择排序
new SelectionSort().SelectionSortVoid(array);
}
public void SelectionSortVoid(int[] array){
int len =array.length;
int min = 0;
for (int i = 0; i <len-1; i++) { // 已排序序列的末尾
min=i;
for (int j = i+1; j <len; j++) { // 未排序序列
if(array[j]<array[min]){
min = j;
}
}
if(min!=i){ //找完一圈后发现未排序里最小的不是当前,则互换
exchange(array, min, i);
}
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
}
}
public static void exchange(int[] array,int i,int j){ //交换位置函数
int temp;
temp = array[i];
array[i]=array[j];
array[j]=temp;
}
}
算法复杂度:(n-1)+(n-2)+…(n-n)=n*n-(1+2+3+…n) =n^2-(n+1)n/2 =1/2(n^2-n) = O(n^2)
选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。
##插入排序
###原理
插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌
对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
###操作步骤
1,从第一个元素开始,该元素可以认为已经被排序,
2,取出下一个元素,在已经排序的元素序列中从后向前扫描
3,如果该元素(已排序)大于新元素,将该元素移到下一位置
4,重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5,将新元素插入到该位置后
6,重复步骤2~5
###代码实现
package 比较排序;
/*
* 分类 ------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
* 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
* 平均时间复杂度 ---- O(n^2)
* 稳定性 ------------ 稳定
*/
public class InsertSort {
public static void main(String[] args) {
int[] array = { 6, 5, 3, 1, 3, 8, 7, 2, 4 };// 从小到大插入排序
new InsertSort().InsertSortVoid(array);
}
public void InsertSortVoid(int[] array) {
int len = array.length;
int insElement = 0;
for (int i = 1; i < len; i++) { // 类似抓扑克牌排序,默认第一张是手牌,之后的len-1张牌要插入手牌
insElement = array[i]; // 拿到的牌是第一张牌,第0张牌是手牌
int j = i - 1;
while (j >= 0 && array[j] > insElement) { // 将抓到的牌与手牌从右向左进行比较,而手牌总是排好序的
array[j + 1] = array[j];
j--;
}
array[j + 1] = insElement;// 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
}
}
}
*最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
*最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
###插入排序的改进:二分插入排序
对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目,我们称为二分插入排序
package 比较排序;
/*
* 分类 ------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
* 最优时间复杂度 ---- O(nlogn)
* 平均时间复杂度 ---- O(n^2)
* 稳定性 ------------ 稳定
*/
public class BinaryInsertSort {
public static void main(String[] args) {
int[] array = { 6, 5, 3, 1, 3, 8, 7, 2, 4 };// 从小到大插入排序
new BinaryInsertSort().BinaryInsertSortVoid(array);
}
public void BinaryInsertSortVoid(int[] array) {
int len = array.length;
for (int i = 1; i < len; i++) { // 抽来的牌
int get = array[i]; // 抽来的牌赋给get
int left = 0; // 拿在左手上的牌总是排序好的,所以可以用二分法
int right = i - 1; // 手牌左右边界进行初始化
while (left <= right) {
int middle = (left + right) / 2;
if (array[middle] < get) {
left = middle + 1;
} else {
right = middle - 1;
}
}
for (int j = i - 1; j >= left; j--) { // 将欲插入新牌位置右边的牌整体向右移动一个单位
array[j + 1] = array[j];
}
array[left] = get; // 将抓到的牌插入手牌
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
}
}
}
当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。
###插入排序的更高效改进:希尔排序(Shell Sort)
尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1,插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
2,希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
package 比较排序;
/*
* 分类 ------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
* 最优时间复杂度 ---- O(n)
* 平均时间复杂度 ---- 根据步长序列的不同而不同。
* 所需辅助空间 ------ O(1)
* 稳定性 ------------ 不稳定
*/
public class ShellSort {
public static void main(String[] args) {
int[] array = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };
new ShellSort().ShellSortVoid(array);
}
public void ShellSortVoid(int[] array) {
int len = array.length;
int h = 0;
while (h <= len) // 生成初始增量
{
h = 3 * h + 1; //h是步长的意思,初始为13,然后为4,然后为1
}
System.out.println(h); //13
while (h >= 1) {
for (int i = h; i < len; i++) { //13刚进来的时候发现不满足,所以执行第37行,h变成了4,第二次执行按2长为4
int j = i - h;
int get = array[i];
while ((j >= 0) && array[j] > get) {
array[j + h] = array[j];
j = j - h;
}
array[j + h] = get;
}
h = (h - 1) / 3; //下一次遍历按步长为1执行,也就是普通的插入排序
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
}
}
}
希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。
比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2时分成两个子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序之前第二个子序列中的8在前面,现在对两个子序列进行插入排序,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,两个8的相对次序发生了改变。
##归并排序
###原理
归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),1945年由冯·诺伊曼首次提出。归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。
###操作步骤
归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
###代码实现
package 比较排序;
/*
* 分类 ------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- O(nlogn)
* 最优时间复杂度 ---- O(nlogn)
* 平均时间复杂度 ---- O(nlogn)。
* 所需辅助空间 ------ O(n)
* 稳定性 ------------ 不稳定
*/
public class MergeSort {
public int[] L = new int[10];
public int[] R = new int[10]; // 两个子数组定义成全局变量(辅助存储空间,大小正比于元素的个数)
public static void main(String[] args) {
int array1[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int array2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int len1 = array1.length;
int len2 = array2.length;
new MergeSort().mergesort_recursion(array1, 0, len1 - 1); // 递归实现
System.out.print("递归实现的归并排序结果:");
for (int i = 0; i < len1; i++) {
System.out.print(array1[i]);
}
new MergeSort().mergesort_iteration(array2, 0, len2 - 1); // 非递归实现
System.out.print("非递归实现的归并排序结果:");
for (int i = 0; i < len2; i++) {
System.out.print(array2[i]);
}
}
public void mergesort_recursion(int[] array1, int left, int right) { // 递归实现的归并排序(自顶向下)
int middle = (left + right) / 2;
if (left < right) {
mergesort_recursion(array1, left, middle);
mergesort_recursion(array1, middle + 1, right);
merge(array1, left, middle, right);
}
}
public static void mergerec(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
=============================================非递归=====================
public void mergesort_iteration(int[] array2, int left, int right) { // 非递归(迭代)实现的归并排序(自底向上)
int low=0;
int middle=0;
int high=0; // 子数组索引,前一个为A[low...middle],后一个子数组为A[middle+1...high]
for (int size = 1; size <= right - left; size *= 2) // 子数组的大小初始为1,每轮翻倍
{
low = left;
while (low + size - 1 <= right - 1 )// 后一个子数组存在(需要归并)
{
middle = low + size - 1;
high = middle + size;
if (high > right) // 后一个子数组大小不足size
high = right;
merge(array2, low, middle, high);
low = high + 1; // 前一个子数组索引向后移动
}
}
}
public void merge(int A[], int left, int middle, int right) {// 合并两个已排好序的数组A[left...middle]和A[middle+1...right]
int n1 = middle - left + 1;
int n2 = right - middle; // 两个数组的大小为n1+1,n2+1,最后一位放置无穷大值
for (int i = 0; i < n1; i++) {
L[i] = A[left + i];
}
for (int j = 0; j < n2; j++)
R[j] = A[middle + j + 1]; // 把两部分分别拷贝到两个数组中
L[n1] = java.lang.Integer.MAX_VALUE; // 使用无穷大作为哨兵值放在子数组的末尾
R[n2] = java.lang.Integer.MAX_VALUE; // 这样可以免去检查某个子数组是否已读完的步骤
int i = 0;
int j = 0;
for (int k = left; k <= right; k++) // 依次比较两个子数组中的值,每次取出更小的那一个放入原数组
{
if (L[i] <= R[j]) {
A[k] = L[i];
i++;
} else {
A[k] = R[j];
j++;
}
}
}
}
只掌握递归的方式就好了迭代的方式理解起来比较复杂。
##堆排序
###原理
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构(通常堆是通过一维数组来实现的),并同时满足堆的性质:即子结点的键值总是小于(或者大于)它的父节点。
###操作步骤
我们可以很容易的定义堆排序的过程:
1,创建一个堆
2,把堆顶元素(最大值)和堆尾元素互换
3,把堆的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
4,重复步骤2,直到堆的尺寸为1
###代码实现
package 比较排序;
public class HeapSort {
public static void main(String[] args) {
int[] array = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大堆排序
int len = array.length;
int heapsize = len; // 堆的大小也就是数组长度
new HeapSort().heapsort(array, heapsize);
System.out.println("堆排序结果:");
for (int i = 0; i < len; i++) {
System.out.print(array[i]);
}
}
public void heapsort(int[] array, int heapsize) {
buildheap(array, heapsize); // 建好了堆(大顶堆)
// ================================================
for (int i = 0; i < heapsize; i++) {
System.out.print(array[i]);
}// 测试点,打印堆:
// ====================================================
for (int i = heapsize; i > 1; i--) { // 堆顶和堆底互换,堆顶的索引是1,但是对应数组array[0]
exchange(array, 0, i - 1); // 将堆顶元素(当前最大值)与堆的最后一个元素互换(该操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法)
heapsize--; // 从堆中去掉最后一个元素
heapify(array, 0, heapsize); // 从新的堆顶元素开始进行堆调整,
}
}
public void buildheap(int[] array, int heapsize) { // 建堆函数
for (int i = heapsize / 2; i >= 1; i--) { // 注意,这里的i用的是堆的标号方式,从1开始。但在堆里的索引1对应数组array[0],所以传入的为i-1
heapify(array, i - 1, heapsize);
}
}
public void exchange(int[] array, int i, int j) // 交换A[i]和A[j]
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public void heapify(int[] array, int i, int heapsize) { // 堆调整函数(这里使用的是最大堆),也就是根是大于左右子树的
int leftchild = 2 * i + 1; // 左孩子索引
int rightchild = 2 * i + 2; // 右孩子索引
int largest = 0; // 选出当前结点与左右孩子之中的最大值
if (leftchild < heapsize && array[leftchild] > array[i]) { // 当前节点的左孩子比当前节点大
largest = leftchild;
} else
largest = i;
if (rightchild < heapsize && array[rightchild] > array[largest]) {
largest = rightchild;
} // 以上的两个if判断出三者之中最大的,没有管谁是第二大的
if (largest != i) {
exchange(array, i, largest); // 把当前结点和它的最大(直接)子节点进行交换
heapify(array, largest, heapsize);
}
}
}
比如序列:{ 9, 5, 7, 5 },堆顶元素是9,堆排序下一步将9和第二个5进行交换,得到序列 { 5, 5, 7, 9 },再进行堆调整得到{ 7, 5, 5, 9 },重复之前的操作最后得到{ 5, 5, 7, 9 }从而改变了两个5的相对次序。时间复杂度为O(nlogn)是因为每次都要重建堆需要logn,共重建了n次
算法中log级别的时间复杂度都是由于使用了分治思想,这个底数直接由分治的复杂度决定.你如果采用二分法,那么就会以2为底数,三分法就会以3为底数,其他亦然.不过无论底数是什么,log级别的渐进意义是一样的.也就是说该算法的时间复杂度的增长与处理数据多少的增长的关系是一样的.
更加详细的介绍参照我的另一篇博客,《数据结构之堆》http://blog.csdn.net/sinat_33087001/article/details/75171434
##快速排序
###原理
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。这里的部分内容参照自该博客,感谢博主:http://developer.51cto.com/art/201403/430986.htm
###操作步骤
快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:
1,从序列中挑出一个元素,作为"基准"(pivot)
2,把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
3,对每个分区递归地进行步骤1~3,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
###举例说明
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:
3 1 2 5 4 6 9 7 10 8
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。
分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字8。
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:
6 1 2 5 9 3 4 7 10 8
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。
此时再次进行交换,交换之后的序列如下:6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。
我们将基准数6和3进行交换。交换之后的序列如下:
3 1 2 5 4 6 9 7 10 8
**注意:**最后那个数如果不是3而是11怎么办?那样就无法和6交换了啊!!
那就成了4和6交换了,这也是为什么j先移动的原因,因为要将交汇点放到基准数左边,所以交汇点一定在比基准数小的地方,所以一定是得由j发现的,所以j要先行,先到达指定汇合点。
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。
左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧
如果你模拟的没有错,调整完毕之后的序列的顺序应该是:
2 1 3 5 4
OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下:
1 2 3 4 5 6 9 7 10 8
对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下
1 2 3 4 5 6 7 8 9 10
到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面上个霸气的图来描述下整个算法的处理过程。
###代码实现
package 比较排序;
/*
* 分类 ------------- 内部比较排序
* 数据结构 ---------- 数组
* 最差时间复杂度 ---- 每次选取的基准都是最大的元素(或者每次都是最小),导致每次只划分出了一个子序列,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
* 最优时间复杂度 ---- 每次选取的基准都能使划分均匀,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
* 平均时间复杂度 ---- O(nlogn)
* 所需辅助空间 ------ O(logn)~O(n),主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度
* 稳定性 ------------ 不稳定
*/
public class Quicksort {
public static void main(String[] args) {
int[] array = { 5, 2, 9, 10, 11, 4, 7, 6, 1, 3, 8, 18, 23, 45 };// 从小到大快速排序
int len = array.length;
new Quicksort().QuicksortVoid(array, 0, len - 1);
System.out.println("快速排序结果:");
for (int i = 0; i < len; i++) {
System.out.print(array[i]);
}
}
public void QuicksortVoid(int[] array, int left, int right) {
int pivot_index; // 基准的索引
if (left < right) {
pivot_index = partition(array, left, right);
QuicksortVoid(array, left, pivot_index - 1); // 基准左边排序
QuicksortVoid(array, pivot_index + 1, right); // 基准右边排序
}
}
private int partition(int[] array, int left, int right) { // 划分函数
int temp = array[left]; // temp中存的就是基准数 ,每次以左边第一个数的下标为基本数
int i = left;
int j = right;
while (i != j) {
// 顺序很重要,要先从右边开始找
while (array[j] >= temp && i < j)
j--;
// 再找左边的
while (array[i] <= temp && i < j)
i++;
// 交换两个数在数组中的位置
if (i < j) {
exchange(array, i, j);
}
}
// 最终将基准数归位
array[left] = array[i];
array[i] = temp;
return i;
}
public static void exchange(int[] array, int i, int j) { // 交换位置函数
int temp;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。
##几种比较算法的总结
###时间复杂度分析
按平均时间将排序分为四类:
1,平方阶(O(n2))排序:一般称为简单排序,例如直接插入、直接选择和冒泡排序;
2,线性对数阶(O(nlgn))排序(因为都采用了分治策略):如快速排序、堆排序和归并排序;
3,O(n)或O(nlogn)阶排序,如希尔排序(是插入排序的优化算法),O(n)或O(nn)阶排序,如鸡尾酒排序(冒泡排序的优化算法);以及O(nlogn)或O(nn)阶排序,如二分插入排序(插入排序的优化算法)
4,线性阶(O(n))排序,如基数排序。
各种排序方法比较
简单排序中直接插入最好,快速排序最快,当文件为正序时,直接插入和冒泡均最佳只需要O(n)。
###稳定性分析及理由
首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。
其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就 是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。
回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。
(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
(2)选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
(4)快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。
(5)归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
(8)堆排序
我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序是不稳定的排序算法。
###使用场景分析
影响排序效果的因素
因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
①待排序的记录数目n;
②记录的大小(规模);
③关键字的结构及其初始状态;
④对稳定性的要求;
⑤语言工具的条件;
⑥存储结构;
⑦时间和辅助空间复杂度等。
不同条件下,排序方法的选择
1,若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
2,若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
3,若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或
归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
4,若要求排序稳定,则可选用归并排序。但从单个记录起进行两两归并的 排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
至此,花费了三天时间总结理解的比较排序算法总结完毕,2017.7.15,下一步目标是对非比较算法做一个总结。加油,在求职的道路上fighting!!!