算法排序执行效率
- 最好情况、最坏情况、平均情况的时间复杂度
- 时间复杂度的系数、常数、低阶
- 比较次数和交换次数
排序算法的内存消耗
算法的内存消耗可以通过空间复杂度来衡量,针对排序算法的空间复杂度,我们有个新的概念,原地排序
。原地排序算法,就是特指空间复杂度为O(1)的排序算法。
排序算法的稳定性
如果待排序的序列中存在值相等的元素,经过排序后,相等元素之间原有的先后顺序不变,我们称这种算法称为稳定的排序算法。
冒泡排序
冒泡排序只会操作相邻的两个数据,每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系的要求。每一次冒泡会让至少一个元素移动到它应该在的位置,重复复n次,就完成了n个数据的排序工作。
public static void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n - 1; i++) {
boolean flag = false;
for (int j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = true;
}
}
if (!flag) break;
}
}
插入排序
我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序的区间数据一直有序。
public static void insertionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 1; i < n; i++) {
int value = a[i];
int j = i - 1;
for (; j >= 0; j--) {
if (a[j] > value) {
a[j + 1] = a[j];
} else {
break;
}
}
a[j + 1] = value;//插入数据
}
}
二分插入排序是优化后的直接插入排序,在新元素插入到已序数组时,采用二分查找方式找到插入位置。
public static void binarySort(int[] a) {
for (int i = 1; i < a.length; i++) {
int temp = a[i];
int left = 0;
int right = i - 1;
//确定要插入的位置
while (left <= right) {
//先获取中间位置
int mid = (left + right) / 2;
if (temp < a[mid]) {
//如果值比中间值小,让right左移到中间下标-1
right = mid - 1;
} else {
//如果值比中间值大,让left右移到中间下标+1
left = mid + 1;
}
}
for (int j = i - 1; j >= left; j--) {
//以左下标为标准,在左位置前插入该数据,左及左后边全部后移
a[j + 1] = a[j];
}
if (left != i) {
//左位置插入该数据
a[left] = temp;
}
}
}
归并排序
利用归并的思想实现的排序算法,该算法用哪个经典的分治策略(分治法中分是将问题问题分成一些小的问题,然后在递归求解,而治的阶段则是将分的阶段得到的各个答案在‘修补’在一起)
private static void sort(int[] a) {
int temp[] = new int[a.length];//排序前,先建好一个等长的临时数组,避免在递归中频繁的开辟空间
sort(a, 0, a.length - 1, temp);
}
private static void sort(int[] a, int left, int right, int temp[]) {
if (left < right) {
int middle = (left + right) / 2;
sort(a, left, middle, temp);//左边归并排序,使得左子序列有序
sort(a, middle + 1, right, temp);//右边归并排序,使得右子序列有序
merge(a, left, right, middle, temp);//将两个有序的子数组合并操作
}
}
private static void merge(int[] a, int left, int right, int middle, int[] temp) {
int i = left;//左序列指针
int j = middle + 1;//右序列指针
int t = 0;//临时数组指针
while (i <= middle && j <= right) {
if (a[i] < a[j]) {
temp[t++] = a[i++];
} else {
temp[t++] = a[j++];
}
}
while (i <= middle) {//将左边的剩余元素填入临时表
temp[t++] = a[i++];
}
while (j <= right) {//将右边的剩余元素填入临时表
temp[t++] = a[j++];
}
t = 0;
//将临时数组中的元素拷贝到原数组
while (left <= right) {
a[left++] = temp[t++];
}
}
快速排序
/**
* 一趟快速排序算法是
* 1.设置初始变量 i=0;j=N-1
* 2.关键值 key = a[0]
* 3.从j开始向前搜索(j--),找到第一个比key小的a[j],交换a[i]和a[j]
* 4.从开始向后搜索(i++),找到第一个比key大的a[i],交换a[j]和a[i]
* 5.重复3、4步骤,知道找到i=j
*
* @param a
* @param start
* @param end
* @return
*/
public static int[] qsort(int[] a, int start, int end) {
int key = a[start];
int i = start;
int j = end;
while (i < j) {
while ((i < j) && a[j] > key) {//想象一下,数组就是一堆树桩,key是特殊的树桩,a[j](右边的)比特殊树桩大的j--,
// a[i](左边的)比特殊树桩小的i++,则是整体数组升序排序渐渐变大,反之降序
j--;
}
while ((i < j) && a[i] < key) {
i++;
}
if (a[i] == a[j] && (i < j)) {
i++;
} else {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
if (i - 1 > start) a = qsort(a, start, i - 1);
if (j + 1 < end) a = qsort(a, j + 1, end);
return a;
}
桶排序
桶排序是将待排序集合处于同一个值域的元素存入同一个桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成多个桶,从值域上看是处于有序状态的。对每个桶中元素进行排序,则所有桶中所有构成的集合是已排序的。
桶排序过程存在两个关键的环节:1元素值域的划分,也就是说元素到桶的映射规则。映射规则需要根据待排序集合的元素分布特性进行选择,若规则设计的过于模糊、宽泛,则可能导致待排序集合中所有元素全部映射到一个桶上,则桶排序相比较排序算法演变。若映射规则设计过于具体、苛刻,则可能导致待排序集合中每个元素值映射到一个桶上,则桶排序向计数排序方式演化。2排序算法的选择,从待排序集合中元素映射到各个桶上的过程,并不存在元素的比较和交换操作,在堆各个桶中元素进行排序时,可以自主选择合适的排序算法,桶排序算法的复杂度和稳定性都根据选择的排序算法不同而不同。
待排序集合为:[-7, 51, 3, 121, -3, 32, 21, 43, 4, 25, 56, 77, 16, 22, 87, 56, -10, 68, 99, 70]
定义映射规则 Γ ( x ) = x 10 − c , 其 中 c = m i n 10 , m i n 为 元 素 最 小 值 , 即 以 间 隔 大 小 10 来 区 分 不 同 值 域 。 每 个 桶 排 序 算 法 为 : 堆 排 序 , 根 据 堆 排 序 特 性 可 知 , K 个 元 素 的 集 合 , 时 间 复 杂 度 为 : K log 2^K , 算 法 不 保 持 稳 定 性 \Gamma(x) =\frac x {10} - c,其中c = \frac {min} {10},min为元素最小值,即以间隔大小10来区分不同值域。每个桶排序算法为:堆排序,根据堆排序特性可知,K个元素的集合,时间复杂度为:K \log \verb!2^K!,算法不保持稳定性 Γ(x)=10x−c,其中c=10min,min为元素最小值,即以间隔大小10来区分不同值域。每个桶排序算法为:堆排序,根据堆排序特性可知,K个元素的集合,时间复杂度为:Klog2^K,算法不保持稳定性
伪代码实现
def bucketSort(arr):
maxium,minum = max(arr),min(arr)
bucketArr = [] for i in range(maxium /10 - minum / 10 +1)
for i in arr: # map every element in array to the corresponding bucket
index = i / 10 - minum / 10
bucketArr[index].append(i)
arr.clear()
for i in bucketArr:
heapSort(i)# sort the elements in every bucket
arr.extends(i) # move the sorted elements in bucket to array
计数排序
计数排序是一个非基于比较的排序算法,它的优势在于在一定的范围内的整数排序时,他的复杂度为O(k+n)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(K)>O(nlogn)的时候其效率反而不如基于比较排序。
计数排序对输入的数据有附加的限制条件:
- 输入的线性表的元素属于有限偏序集S;
- 设输入的线性表的长度为n,|S|=k (表示集合S中的元素的总数目为K),则k=O(n),这两个条件下计数排序的复杂性为O(n)。
public static int[] countSort(int[] a) {
int[] b = new int[a.length];
int max = a[0], min = a[0];
for (int i : a) {
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
int k = max - min + 1; //这里k的大小是要排序的数组中元素大小的极差+1
int c[] = new int[k];
for (int i = 0; i < a.length; i++) {
c[a[i] - min] += 1;//记录相同元素在数组中的个数
}
for (int i = 1; i < c.length; ++i) {
c[i] = c[i] + c[i - 1]; // 数组顺序累加
}
for (int i = a.length - 1; i >= 0; i--) {
b[--c[a[i] - min]] = a[i];
}
return b;
}
基数排序
基数排序属于‘分配式排序’,又称‘桶子法’,它是透过键值得部分资讯,将要排序的元素分配至某些‘桶’中,籍以达到排序的作用,基数排序算法属于稳定性的排序,其时间复杂度为O(nlog®m),其中r为所采取的基数,而m为堆数,在某些情况下,基数排序法的效率高于其他的稳定性排序法。
/**
* 基数排序
*
* @param arr
* @param d
*/
public static void RadixSort(int[] arr, int d) {//d表示最大有多少位
int k = 0;//重排时数组下标
int n = 1;
int m = 1;//控制排序在哪一位
int[][] temp = new int[10][arr.length];//数组的第一维表示可能的余数0-9
int order[] = new int[10];//表示在该位是i的个数
while (m <= d) {
for (int i = 0; i < arr.length; i++) {
int lsd = (arr[i] / n) % 10;
temp[lsd][order[lsd]] = arr[i];
order[lsd]++;
}
for (int i = 0; i < 10; i++) {
if (order[i] != 0) {
for (int j = 0; j < order[i]; j++) {
arr[k] = temp[i][j];
k++;
}
}
order[i] = 0;
}
n *= 10;
m++;
k = 0;
}
}