交换排序
交换,指的是根据序列中两个关键字的比较结果来对换这两个记录在序列中的位置,主要有冒泡排序与快速排序。
冒泡排序(Bubble Sort)
冒泡排序的基本思想:从前往后或者从后往前,对相邻的两个元素进行比较,若逆序,则交换。每次冒泡排序都会让至少一个元素移动到它应该在的位置,重复n-1,就完成了对n个数据的排序。
如果对一组数据7,8,9,6,5,4,从小到大排序,第一次冒泡排序的详细过程如下所示:
可以看出一次冒泡操作后,有一个元素已经移动到应该在的位置上了,经过n-1次这样的冒泡操作后,n-1个元素被移动到应该在的位置上了,剩下一个元素也自然在应该在位置上。
实际上,刚刚的冒泡过程还可以优化。当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
#include <stdio.h>
#include <stdlib.h>
// 冒泡排序c实现,a表示数组,n表示数组大小
/**
* Author: gamilian
*/
void bubble_sort(int a[], int n) {
if (n <= 1)
return;
for (int i = 0; i < n; ++i) {
boolean flag = false; // 提前退出冒泡循环的标志位
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag)
break; // 没有数据交换,提前退出
}
}
# 冒泡排序python实现
"""
Author: gamilian
"""
def bubble_sort(a):
""" 冒泡排序
args:
a: List[int]
"""
length = len(a)
if length <= 1:
return
for i in range(length):
made_swap = False
for j in range(length - i - 1):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
made_swap = True
if not made_swap:
break
算法的稳定性:在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。
空间复杂度 :冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。
时间复杂度:最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是 O(n)。而最坏的情况是,要排序的数据刚好是倒序排列的,我们需要进行 n -1次冒泡操作,所以最坏情况时间复杂度为 O(n^2)。而平均情况下的时间复杂度比较复杂,可以通过逆序度来计算。
逆序度是数组中具有无序关系的元素对的个数。
逆序元素对:a[i] > a[j], 如果i < j。
有序度是数组中具有有序关系的元素对的个数。
有序元素对:a[i] <= a[j], 如果i < j。
对于一个完全有序的数组,有序度就是 n*(n-1)/2。我们把这种完全有序的数组的有序度叫作满有序度。
同时,逆序度 = 满有序度 - 有序度。
冒泡排序包含两个操作原子,比较和交换。每交换一次,有序度就加 1。不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是n*(n-1)/2–初始有序度。
对于包含 n 个数据的数组进行冒泡排序,最坏情况下,初始状态n个数据逆序,有序度为 0,所以要进行 n*(n-1)/2 次交换。最好情况下,初始状态数据有序,有序度为 n*(n-1)/2,就不需要进行交换。平均情况下,需要 n*(n-1)/4 次交换操作,而比较操作肯定要比交换操作多,而复杂度的上限是 O(n^2),所以平均情况下的时间复杂度就是 O(n^2)。
快速排序(Quick sort)
快速排序的思想是基于分治思想的: 如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
递推公式:quick_sort(left…right) = quick_sort(left…pivot -1) + quick_sort(pivot +1… right)
终止条件:left >= right
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
// 快排c实现
/**
* Author: gamilian
*/
// 对区间[left,right]划分,采用随机pivot
int Partition(int A[], int left, int right){
int p = (round(1.0 * rand() / RAND_MAX * (right - left) + left)); //生成[left,right]内的随机数
int temp = A[left]; //交换A[p]与A[left]
A[left] = A[p];
A[p] = temp;
int pivot = A[left]; //设置随机元素,即现在的第一个元素为pivot
while(left < right){ //只要left与right不相遇
while(left < right && A[right] > pivot)
right--; //只要right比pivot大,就一直左移
A[left] = A[right]; //将比pivot小的元素移到左边
while(left < right && A[left] <= pivot)
left++; //只要left比pivot小,就一直右移
A[right] = A[left]; //将比pivot大的元素移到右边
}
A[left] = pivot; //pivot放在最终left与right相遇的位置
return left; //返回存放pivot的下标
}
// A是数组,left与right初值为序列首尾下标
void quick_sort(int A[], int left, int right){
if(left < right){ //当前区间长度超过1
int pivot = Partition(A, left, right); //划分区间
quick_sort(A, left, pivot - 1); //对于左子区间快排
quick_sort(A, pivot + 1, right); //对右子区间快排
}
}
# 快排python实现,划分时用swap
"""
Author: gamilian
"""
import random
def quick_sort(a):
""" 快速排序
args:
a: List[int]
"""
length = len(a)
quick_sort_between(a, 0, length - 1)
def quick_sort_between(a, left, right):
""" 将a的[left,right]区间快排
args:
a: List[int]
left: int
right: int
"""
if left < right:
pivot = partition(a, left, right)
quick_sort_between(a, left, pivot - 1)
quick_sort_between(a, pivot + 1, right)
def partition(a, left, right):
""" 划分区间
args:
a: List[int]
left: int
right: int
"""
# 随机pivot
temp = random.randint(left, right)
a[left], a[temp] = a[temp], a[left]
pivot, j = a[left], left
for i in range(left + 1, right + 1):
if a[i] <= pivot:
j += 1
a[j], a[i] = a[i], a[j] # swap
a[left], a[j] = a[j], a[left]
return j
# 双向快排python实现,划分时用swap
"""
Author: gamilian
"""
import random
def quick_sort_twoway(a):
""" 双向排序
args:
a: List[int]
"""
# 双向排序: 提高非随机输入的性能
# 不需要额外的空间,在待排序数组本身内部进行排序
# 基准值通过random随机选取
# 入参: 待排序数组, 数组开始索引 0, 数组结束索引 len(a)-1
length = len(a)
if a is None or length < 1:
return a
def quick_sort_twoway_between(a, left, right):
# 小数组排序i可以用插入或选择排序
# if right-left < 50 : return a
# 基线条件: left index = right index; 也就是只有一个值的区间
if left >= right:
return a
# 随机选取基准值, 并将基准值替换到数组第一个元素
temp = random.randint(left, right)
a[left], a[temp] = a[temp], a[left]
pivot = a[left]
# 缓存边界值, 从上下边界同时排序
i, j = left, right
while True:
# 第一个元素是基准值,所以要跳过
i += 1
# 在小区间中, 进行排序
# 从下边界开始寻找大于基准值的索引
while i <= right and a[i] <= pivot:
i += 1
# 从上边界开始寻找小于基准值的索引
# 因为j肯定大于i, 所以索引值肯定在小区间中
while a[j] > pivot:
j -= 1
# 如果小索引大于等于大索引, 说明排序完成, 退出排序
if i >= j:
break
a[i], a[j] = a[j], a[i]
# 将基准值的索引从下边界调换到索引分割点
a[left], a[j] = a[j], a[left]
quick_sort_twoway_between(a, left, j - 1)
quick_sort_twoway_between(a, j + 1, right)
return a
return quick_sort_twoway_between(a, 0, length - 1)
算法的稳定性:因为分区的过程涉及交换操作,如果数组中有两个相同的元素,在经过第一次分区操作之后,两个相同元素的相对先后顺序就会改变。所以,快速排序并不是一个稳定的排序算法。
空间复杂度:如果算上递归工作栈,由于快排是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量与递归调用的最大深度一致。快排最好情况空间复杂度为 O(nlogn),最坏情况,要进行n-1次递归调用,即快排最坏情况空间复杂度为 O(n),快排平均情况空间复杂度为 O(nlogn)。
如果不算上递归工作栈,则快排空间复杂度为 O(1),是一个原地排序算法。
时间复杂度:如果每次分区操作,都能正好把数组分成大小接近相等的两个小区间,那快排的时间复杂度递推求解公式跟归并是相同的。所以,快排最好情况时间复杂度为 O(nlogn)。
T(1) = C; n=1时,只需要常量级的执行时间,所以表示为C。T(n) = 2*T(n/2) + n; n>1
如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n/2 个元素,所以,快排最坏情况时间复杂度为 O(n^2)。
假设每次分区操作都将区间分成大小为 9:1 的两个小区间。
T(1) = C; n=1时,只需要常量级的执行时间,所以表示为C。
T(n) = T(n/10) + T(9*n/10) + n; n>1
所以,快排平均时间复杂度为O(nlogn)