目录
冒泡排序
- 从头开始比较每一对相邻元素,如果第1个比第2个大,就交换它们的位置
- 执行完一轮后,最末尾那个元素就是最大的元素
- 忽略 1 中曾经找到的最大元素,重复执行步骤 1,直到全部元素有序
基础写法
public void sort(int[] array) {
for (int end = array.length - 1; end > 0; end--) {
for (int begin = 1; begin <= end; begin++) {
if ((array[begin] < array[begin - 1]) {
// 交换下标为begin和begin - 1的元素
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
}
}
}
}
优化一
如果序列已经完全有序,可以提前终止冒泡排序
public void sort(int[] array) {
for (int end = array.length - 1; end > 0; end--) {
// 定义一个升序标志
boolean sorted = true;
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
// 交换下标为begin和begin - 1的元素
swap(begin, begin - 1);
sorted = false;
}
}
if (sorted) break;
}
}
优化二
如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次数
public void sort(int[] array) {
for (int end = array.length - 1; end > 0; end--) {
// 记录每一轮最后一次交换的位置
int sortedIndex = 1;
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
// 交换下标为begin和begin - 1的元素
swap(begin, begin - 1);
sortedIndex = begin;
}
}
// 更新end
end = sortedIndex;
}
}
复杂度分析
- 最坏、平均时间复杂度:O(n2)
- 最好时间复杂度:O(n)
- 空间复杂度:O(1)
- 属于稳定排序
选择排序
- 从序列中找出最大的那个元素,然后与最末尾的元素交换位置
- 执行完一轮后,最末尾的那个元素就是最大的元素
- 忽略 1 中曾经找到的最大元素,重复执行步骤 1
基础写法
public void sort(int[] array) {
for (int end = array.length - 1; end > 0; end--) {
// 记录每一轮最大值
int max = 0;
for (int begin = 1; begin <= end; begin++) {
if (array[max] < array[begin]) {
max = begin;
}
}
// 交换记录最大值下标和begin的元素
swap(max, end);
}
}
复杂度分析
-
最好、最坏、平均时间复杂度: O(n 2 )
-
空间复杂度: O(1)
-
属于不稳定排序
插入排序
- 在执行过程中,插入排序会将序列分为2部分
- 头部是已经排好序的,尾部是待排序的
- 从头扫描每一个元素
- 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
基础写法
public void sort(int[] array) {
for(int begin = 1; begin < array.length; begin++) {
// 依次从下标为 1 的数开始排列顺序
int cur = begin;
while(cur > 0 && array[cur] < array[cur - 1]) {
swap(cur, cur - 1);
// 当前数的下标--,继续进行比较
cur--;
}
}
}
注意:
- 插入排序的时间复杂度与逆序对的数量成正比关系,逆序对的数量越多,插入排序的时间复杂度越高
- 当逆序对的数量极少时,插入排序的效率特别高
-
数据量不是特别大的时候,插入排序的效率也是非常好的
优化一
思路是将【交换】转为【
挪动
】,将三行语句变为一行
- 先将待插入的元素备份
- 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置
- 将待插入元素放到最终的合适位置
public void sort(int[] array) {
for(int begin = 1; begin < array.length; begin++) {
// 记录合适位置的下标
int cur = begin;
// 记录要插入元素的值
int v = array[cur];
while(cur > 0 && v - array[cur - 1]) < 0) {
array[cur] = array[cur - 1];
cur--;
}
// 将待插入元素放到最终的合适位置
array[cur] = v;
}
}
优化二
二分搜索
如何确定一个元素在数组中的位置?
- 如果是无序数组,从第 0 个位置开始遍历搜索,平均时间复杂度:O(n)
- 如果是有序数组,可以使用二分搜索,最坏时间复杂度:O(logn)
思路
◼
假设在 [
begin
,
end
) 范围内搜索某个元素
v
,
mid
== (
begin
+
end
) / 2
◼ 如果
v
<
m,去 [
begin
,
mid
) 范围内二分搜索
◼
如果
v
>
m,去 [
mid
+ 1,
end
) 范围内二分搜索
◼
如果
v
==
m,直接返回
mid
◼ 没有找到则返回-1
public static int indexOf(int[] array, int v) {
if (array == null || array.length == 0) return -1;
int begin = 0;
int end = array.length;
while (begin < end) {
// 位运算取中值
int mid = (begin + end) >> 1;
if (v < array[mid]) { // 小于中值end变
end = mid;
} else if (v > array[mid]) { // 大于中值begin变
begin = mid + 1;
} else {
return mid; //相等返回
}
}
return -1; // 未找到返回-1
}
查找待插入的位置
假设在 [
begin
,
end
) 范围内搜索某个元素
v
,
mid
== (
begin
+
end
) / 2
- 如果 v < m,去 [begin, mid) 范围内二分搜索
- 如果 v ≥ m,去 [mid + 1, end) 范围内二分搜索
- 最终返回该位置的下标,没有找到则返回第一个大于v的元素位置
public static int search(int[] array, int v) {
if (array == null || array.length == 0) return -1;
int begin = 0;
int end = array.length;
while (begin < end) {
int mid = (begin + end) >> 1;
if (v < array[mid]) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin; // == end
}
实现
public void sort(int[] array) {
for (int begin = 1; begin < array.length; begin++) {
insert(begin, search(begin));
}
}
// 将source位置的元素插入到dest位置
private void insert(int source, int dest) {
T v = array[source];
for (int i = source; i > dest; i--) {
array[i] = array[i - 1];
}
array[dest] = v;
}
/**
* 利用二分搜索找到 index 位置元素的待插入位置
* 已经排好序数组的区间范围是 [0, index)
*/
private int search(int index) {
int begin = 0;
int end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (cmp(array[index], array[mid]) < 0) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
注意:
使用了二分搜索后,只是减少了比较次数,但插入排序的平均时间复杂度依然是 O(n2)
复杂度分析
- 最好、最坏、平均时间复杂度:O(n2)
- 空间复杂度:O(1)
- 属于稳定排序
归并排序
1945年由
约翰·冯·诺伊曼(John von Neumann)
首次提出
执行流程:
- 不断地将当前序列平均分割成2个子序列,直到不能再分割(序列中只剩1个元素)
- 不断地将2个子序列合并成一个有序序列,直到最终只剩下1个有序序列
merge细节
需要 merge 的 2 组序列存在于同一个数组中,并且是挨在一起的
为了更好地完成 merge 操作,最好将其中 1 组序列备份出来,比如 [begin, mid)
- 左边先结束,则数组已经排序完毕,因为两边数组都是有序的
- 右边先结束,则只需把左边数组移到右边即可
代码实现
public void sort() {
leftArray = new int[array.length >> 1];
sort(0, array.length);
}
// 对 [begin, end) 范围的数据进行归并排序
private void sort(int begin, int end) {
// 元素数量 <2
if (end - begin < 2) return;
// 中间值
int mid = (begin + end) >> 1;
sort(begin, mid); // 递归左边排序
sort(mid, end); // 递归右边排序
merge(begin, mid, end); // 归并操作
}
// 将 [begin, mid) 和 [mid, end) 范围的序列合并成一个有序序列
private void merge(int begin, int mid, int end) {
int lb = 0, le = mid - begin; // 左边数组左边界,右边界
int rb = mid, re = end; //右边数组左边界,有边界
int ab = begin; // 往哪个位置覆盖,从begin开始
// 备份左边数组
for (int i = lb; i < le; i++) {
// 传进来的参数为begin,不一定是0
leftArray[i] = array[begin + i];
}
// 如果左边还没有结束,右边结束则排序完成
while (lb < le) {
// cmp改为 <= 0 会失去稳定性
// 右边比左边小
if (rb < re && cmp(array[rb], leftArray[lb]) < 0) {
array[ab++] = array[rb++]; // 拷贝右边数组到array
} else { // 右边大于等于左边先拷贝左边,保证稳定性
array[ab++] = leftArray[lb++]; // 拷贝左边数组到array
}
}
}
复杂度分析
- 由于归并排序总是平均分割子序列,所以最好、最坏、平均时间复杂度都是O(nlogn)
- 属于稳定排序归并排序的空间复杂度是On/2+logn=O(n),n/2用于临时存放左侧数组,logn是因为递归调用
- 属于稳定排序