选择排序
思路:
从最左边的元素开始,逐个遍历,找到这一趟中的最小值,存到minIndex里面,最后这一趟结束,将它和这一趟开始遍历的位置i上的元素swap,直到最后一个位置上也就位。相当于选择出每一趟最小的元素,然后把它放到这一趟开始的地方。
基础版:
void selectionSort(int arr[], int n){
for(int i = 0 ; i < n ; i ++){
// 寻找[i, n)区间里的最小值
int minIndex = i;
for( int j = i + 1 ; j < n ; j ++ )
if( arr[j] < arr[minIndex] )
minIndex = j;
swap( arr[i] , arr[minIndex] );
}
}
插入排序
思路:
由于第一个元素可以默认为已排好序,因此可以从第二个元素开始遍历,每一趟使一个元素正确的插入到前面已排好序的数组中。如果这个元素比它前面的元素小,则swap一下,如果大于等于它前面的元素,说明它以及它前面的元素已经排好序了,这一趟结束。
插入排序和选择排序一个很大的区别就是插入排序每一趟有可能提前结束,即它只要到达一个位置大于等于它前面的元素,那么这一趟就已经排好了;而选择排序每一趟必须遍历完这一趟所有的元素,找到最小的那个,然后再把最小的那个和这一趟开始的元素进行swap,是不能提前结束的。
对于近乎有序的数组,适合用插入排序。
对于完全有序的数组,插入排序复杂度为O(n)O(n)。
基础版:
void insertionSort(T arr[], int n){
for( int i = 1 ; i < n ; i ++ ) {
// 寻找元素arr[i]合适的插入位置
// 写法1
for( int j = i ; j > 0 ; j-- ) //注意这里j遍历到下标为1的位置就好了,因为下面是将位置j和位置j-1的元素进行比较
if( arr[j] < arr[j-1] )
swap( arr[j] , arr[j-1] );
else
break;
// 写法2
// for( int j = i ; j > 0 && arr[j] < arr[j-1] ; j -- )
// swap( arr[j] , arr[j-1] );
}
return;
}
改进版:
将原本一次次的swap操作,变成一次次比较,而在一趟的最后进行赋值。
插入排序算法针对完全排好序的数组是O(n)级别的算法,每次判断一下当前数就是正确位置,这一趟就退出了
改进后的插入排序避免了很多次unnecessary copy,由此非常适合基本排好序而不需要很多次变动的数组。
void insertionSort(T arr[], int n){
for( int i = 1 ; i < n ; i ++ ) {
// 写法3
T e = arr[i];// 声明一个变量保存要插入的元素
int j; // j保存元素e应该插入的位置
for (j = i; j > 0 && arr[j-1] > e; j--)//如果这个元素前面一个元素比它要大且未遍历到边界
arr[j] = arr[j-1];把这个位置前面一个元素往这个位置挪
arr[j] = e;//找到位置,赋值
}
return;
}
冒泡排序
思路:
i从最左边的元素开始遍历,一共要遍历n-1趟,因为第n-1趟遍历完成后,最后一个元素也就位了。在每一趟里面,j用来从第二个元素开始遍历到这一趟的最后一个元素,然后将它与它前面的元素比较,如果比前面的元素小就交换,然后j++;如果大于等于前面的元素就不交换,然后j++。直到遍历到这一趟的最后一个元素为止。
如,
第一趟:i = 0, j下标从1遍历到n-1,最后一个元素就位;
第二趟:i = 1, j从1遍历到n-2,最后一个元素就位
…
第m趟:i = m-1, j从1遍历到n-1-i
…
第n-1趟:i = n-2, j从1遍历到n-1-n+2=1
基础版:
void bubbleSort(T arr[], int n){
for(int i = 0; i < n-1 ; i++){ // n-1趟即可
for(int j = 1; j < n-i; j++){
if(arr[j] < arr[j-1])
swap(arr[j], arr[j-1]);
}
}
}
希尔排序
思路:
希尔排序的思路实际上是插入排序的延伸。插入排序中每一次都和之前的一个元素比较,而希尔排序每一次对之前的第h个元素进行比较,这样通过将h一个很大的值,逐渐变成一个很小的值,直到等于1,此时整个数组也排好了。
基础版:
void shellSort(T arr[], int n){
// 计算 increment sequence: 1, 4, 13, 40, 121, 364, 1093...
int h = 1;
while( h < n/3 )
h = 3 * h + 1;
while( h >= 1 ){
// h-sort the array
for( int i = h ; i < n ; i ++ ){
// 对 arr[i], arr[i-h], arr[i-2*h], arr[i-3*h]... 使用插入排序
T e = arr[i];
int j;
for( j = i ; j >= h && e < arr[j-h] ; j -= h )
arr[j] = arr[j-h];
arr[j] = e;
}
h /= 3;
}
}
// 比较SelectionSort, InsertionSort和BubbleSort和ShellSort四种排序算法的性能效率
// ShellSort是这四种排序算法中性能最优的排序算法
上面的排序算法都是O(n2)O(n2)级别的算法,下面介绍的归并排序和快速排序是O(nlogn)O(nlogn)级别的,那么O(n2)O(n2)和O(nlogn)O(nlogn)有什么不同呢?
归并排序
思路:
归并排序利用递归的思想,把数组分成两组,而后再在组内再分成两组,以此类推直到每组只有一个元素,此时每组都是排好序的。之后再两组两组进行merge操作。
需要申请额外空间,使得归并的时候时间复杂度是O(n)。
注意数组越界和范围定义的问题。
基础版:
template<typename T>
void mergeSort(T arr[], int l, int r){ //递归使用归并排序,对arr[l...r]范围进行排序
if(l >= r)
return;
int mid = l + (r-l)/2;
mergeSort(arr,l,mid);
mergeSort(arr,mid+1,r);
merge(arr, l, mid, r);
}
void merge(T arr[], int l, int mid, int r){ // 将arr[l...mid]和arr[mid+1,r]两部分进行归并
T aux[r-l+1];
for(int i = l ; i <= r; i++)
aux[i-l] = arr[i]; //aux数组和arr数组的偏移量是l
int i = l, j = mid + 1;
for(int k = l; k <= r; k++){
//要考虑数组越界:越界情况是i>mid和j>r
if(i > mid){
arr[k] = aux[j-l];
j++;
}
else if ( j > r){
arr[k] = aux[i-l];
i++;
}
//都没有越界的情况
if(aux[i-l] < aux[j-l]){
arr[k] = aux[i-l];
i++;
}
else{
arr[k] = aux[j-l];
j++;
}
}
}
改进版:
- 对于近乎有序的数组,适合用插入排序,此时用归并排序反而效率很低,因此我们要对它进行优化。在mergeSort函数里,当两个子数组都sort了之后,且arr[mid]<=arr[mid+1],则无需进行merge操作,因为arr[l…mid]和arr[mid+1…r]都已经排好序了,而arr[mid]又小于等于arr[mid+1],此时整个数组就已经排好序了。在代码中只需加一行判断即可。
void mergeSort(T arr[], int l, int r){
if(l >= r)
return;
int mid = l + (r-l)/2;
mergeSort(arr, l, mid);
mergeSort(arr, mid+1, r);
if(arr[mid] > arr[mid+1])//merge之前判断
merge(arr,l,mid,r);
}
- 当subarray比较小的时候,我们可以转而使用插入排序而非归并排序来提高性能,这是因为此时很大概率这个subarray是近乎有序的数组。
void mergeSort(T arr[], int l, int r){
if(r - l <= 15){//数组元素少的时候采用插入排序
insertionSort(arr, l, r);
return;
}
int mid = l + (r-l)/2;
mergeSort(arr, l, mid);
mergeSort(arr, mid+1, r);
if(arr[mid] > arr[mid+1])//merge之前判断
merge(arr,l,mid,r);
}
之前的插入排序是从0…n-1进行排序的,这里的插入排序是从l到r进行排序的,细节上略有不同:
void insertionSort(T arr[], int l, int r){
for(int i = l+1; i <= r; i++){
T e = arr[i];
int j;
for(j = i; j > l && arr[j-1] > e; j--)
arr[j] = arr[j-1];
arr[j] = e;
}
return;
}
总结
需要注意的是,快速排序的空间复杂度是O(logn)O(logn),这是因为它使用递归需要开辟额外的栈空间来存储上一状态,递归的次数是lognlogn级别的。