文章目录
一、综述
(一)、排序概念
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。
(二)、分类
- 稳定排序:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。其中冒泡,插入,基数,归并属于稳定排序,选择,快速,希尔,归属于不稳定排序。
- 就地排序:若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间为O(1),则称为就地排序。
(三)、总览
说明:排序算法众多,这里只介绍几种常见排序算法
二、插入排序
(一)、思路
用 bound 下标将数组分为两个部分,[0,bound) 和 [bound,size) 两个部分,前者是已排序区间,后者是未排序区间,每次从未排序区间把 bound 位置的元素拿出来插入在已排序区间的正确位置。
注:需要循环 (array.length -1)次 来为未排序区间的每一个元素在已排序区间找位置
(二)、代码
public static void insertSort(int[] array) {
// 1.找位置
for (int bound = 1; bound < array.length; bound++) {
int v = array[bound]; // 准备要插入的元素
int cur = bound - 1; // 在已排序元素序列中为待排序元素找要插入的位置
for (; cur >= 0; cur--) {
if (array[cur] > v) {
array[cur + 1] = array[cur];
} else {
// 说明此时已经找到了合适的位置
break;
}
}
// 将bound位置的元素插入进去
// 始终有一个空位置,就是下标 cur + 1 ,初始的空位置是 bound = cur + 1;cur 在不断变
array[cur + 1] = v;
}
}
(三)、算法复杂度
时间复杂度 | 空间复杂度 | ||
---|---|---|---|
最好 | 平均 | 最坏 | O(1) |
O(n) | O(n^2) | O(n^2) |
插入排序,初始数据越接近有序,时间效率越高
三、希尔排序
(一)、思路
将无序序列分为 gap 组,每一组进行插入排序,这样不断地减少组数来达到排序效果。从结果来看,gap 每减小一次,数组地有序性就会得到增强,直至完全有序。
(二)、代码
public static void shellSort(int[] array) {
// gap 怎么设计?
int gap = array.length / 2;
// 1.当gap > 1 地时候,就在每一小组内进行组内排序
while (gap > 1) {
insertSortGap(array, gap);
gap /= 2;
}
// 2.最后在全部序列上进行一次插入排序
insertSortGap(array, 1);
}
private static void insertSortGap(int[] array, int gap) {
// 循环处理
for (int bound = 1; bound < array.length; bound++) {
int val = array[bound]; // 要处理的一个数字
int cur = bound - gap;
for (; cur >= 0; cur = cur - gap) {
if (array[cur] > val) {
array[cur + gap] = array[cur];
} else {
// 已经找到 v 要插入的地方
break;
}
}
array[cur + gap] = val;
}
}
(三)、算法复杂度
时间复杂度 | 空间复杂度 | ||
---|---|---|---|
最好 | 平均 | 最坏 | O(1) |
O(n) | O(n^1.3) | O(n^2) |
三、选择排序
(一)、思路
我们将无序序列分为有序(让前面一部分有序)和无序两部分,不断在无序数组中找到最小的元素放在有序序列的最后一个的后面(此时无序序列的开头),不断在无序数组中最小直至最后一个元素。
(二)、代码
// 每次在数组中找到最小的元素放在待排序部分的开头
public static void selectSort(int[] array) {
// 循环 length 次,找到每一个位置的需要插入的元素
for (int bound = 0; bound < array.length; bound++) {
// 从 bound + 1;开始找最小的元素,然后放在 bound 位置
for (int cur = bound + 1; cur < array.length; cur++) {
// 挑选出来的最小元素,需要放在下标为 bound 的位置
if (array[cur] < array[bound]) {
int tmp = array[cur];
array[cur] = array[bound];
array[bound] = tmp;
}
}
}
}
(三)、算法复杂度
时间复杂度 | 空间复杂度 |
---|---|
O(n) | O(1) |
数据不敏感 | 数据不敏感 |
四、堆排序
(一)、思路
在传进来数组上直接搞一个大堆,然后将堆的第一个和当前堆最后一个交换。(注意,堆在不断缩小,有序性在不断增加)这样不断重复,就能够从后到前使数组有序。
注意:升序建立大堆,降序建立小堆
(二)、代码
public static void heapSort(int[] array) {
// 1.用传进来的数组建立一个大堆
createHeap(array);
// 循环 array.length - 1 次,最后一次它本身,不用交换
for (int i = 0; i < array.length - 1; i++) {
// 2.将第一个和最后一个(相对)元素交换
// 相对位置的最后一个下标始终向前移动一个位置
// 堆中的元素个数
int heapSize = array.length - i;
// 交换元素,使得后边的序列有序
swap(array, 0, heapSize - 1);
// 交换完成使堆中的元素减 1
heapSize--;
// 3.将剩余部分重新向下调整使保持堆结构
shiftDown(array, heapSize, 0);
}
}
// 用传进来的数组构建一个大堆
private static void createHeap(int[] array) {
// 从后往前对每一个元素进行向下调整
for (int index = (array.length - 1 - 1) / 2; index >= 0; index--) {
shiftDown(array, array.length, index);
}
}
// 交换数组中两个元素
private static void swap(int[] array, int a, int b) {
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
// 向下调整
public static void shiftDown(int[] array, int size, int index) { // 大堆
// 将需要调整的位置初始化为parent
int parent = index;
int child = parent * 2 + 1;
while (child < size) { // 保证 child 不能越界
// 使 child 一定是左右孩子中最大的那个
if (child + 1 < size && array[child + 1] > array[child]) {
child = child + 1;
}
// 然后用最大的那个 child 和 parent 比较
if (array[parent] < array[child]) {
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
} else {
// 结束
break;
}
parent = child;
child = 2 * parent + 1;
}
}
(三)、算法复杂度
时间复杂度 | 空间复杂度 |
---|---|
O(n * log(n)) | O(1) |
数据不敏感 |
五、冒泡排序
(一)、思路
在无序序列中循环比较相邻两个元素的的值,这里,我们从后往前比较,将小的值不断交换到前边有序序列中
(二)、代码
public static void bubbleSort(int[] array) {
// 按照每次找最小的方式来进行排序. (从后往前比较交换)
for (int bound = 0; bound < array.length; bound++) {
// [0,bound) 已经排序区间
// [bound,size) 待排序算法
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur] < array[cur - 1]) {
int tmp = array[cur];
array[cur] = array[cur - 1];
array[cur - 1] = tmp;
}
}
}
}
(三)、算法复杂度
时间复杂度 | 空间复杂度 | ||
---|---|---|---|
最好 | 平均 | 最坏 | O(1) |
O(n) | O(n^2) | O(n^2) |
六、快速排序
(一)、思路
快速排序:借助递归来完成
1.在待排序区间中找一个基准值(一般为区间的第一个或者最后一个元素)
2.以基准值为中心,将整个区间整理成三个部分:左侧元素都小于基准值,右侧元素都大于基准值
3.再次针对左侧和右侧区间,重复以上步骤进行递归
(二)、代码
1,递归写法
public static void quickSort(int[] array) {
// 辅助完成递归过程,这里使用的闭区间
quickSortHelper(array, 0, array.length - 1);
}
// 辅助函数 : [left , right]
private static void quickSortHelper(int[] array, int left, int right) {
if (left >= right) {
// 此时,区间有 0 或者 1 个元素,不用排序
return;
}
int index = partition(array, left, right);
quickSortHelper(array, left, index - 1);
quickSortHelper(array, index + 1, right);
}
/*
partition 方法做:
取出一个基准值,让小于基准值的放在基准值左边,大于基准值的放在右边
并且返回基准值在这样一次调整中的位置
*/
private static int partition(int[] array, int left, int right) {
int beg = left;
int end = right;
// 取最右侧元素为基准值
int base = array[right];
while (beg < end) {
// 从左往右找比基准值大的元素
while (beg < end && array[beg] <= base) {
beg++;
}
// 从右往左找比基准值比基准值小的元素
while (beg < end && array[end] >= base) {
end--;
}
// 执行完上面的步骤,beg 要么和end重合,要么end就指向一个小于base的值
// 交换beg 和 end 的值
swap(array, beg, end);
}
swap(array, beg, right);
return beg;
}
private static void swap(int[] array, int beg, int end) {
int tmp = array[beg];
array[beg] = array[end];
array[end] = tmp;
}
2,非递归写法(借助栈)
public static void quickSortByLoop(int[] array) {
// 借助栈来实现递归过程
Stack<Integer> stack = new Stack<>();
// 初始情况下,先把左右边界进行入栈(先右后左),左右边界仍然构成前闭后开区间
stack.push(array.length - 1);
stack.push(0);
while (!stack.isEmpty()) {
// 注意和 push 的顺序正好相反
int left = stack.pop();
int right = stack.pop();
if (left > right) {
// 区间中只有 1个 或者 0 个
continue;
}
// 通过 partition 把区间整理成以基准值问中心的形式
int index = partition(array, left, right);
// [index + 1,right] 基准值右侧区间
stack.push(right);
stack.push(index + 1);
// [left ,index] 基准值左侧区间
stack.push(index-1);
stack.push(left);
}
/*
partition 方法做:
取出一个基准值,让小于基准值的放在基准值左边,大于基准值的放在右边
并且返回基准值在这样一次调整中的位置
*/
private static int partition(int[] array, int left, int right) {
int beg = left;
int end = right;
// 取最右侧元素为基准值
int base = array[right];
while (beg < end) {
// 从左往右找比基准值大的元素
while (beg < end && array[beg] <= base) {
beg++;
}
// 从右往左找比基准值比基准值小的元素
while (beg < end && array[end] >= base) {
end--;
}
// 执行完上面的步骤,beg 要么和end重合,要么end就指向一个小于base的值
// 交换beg 和 end 的值
swap(array, beg, end);
}
swap(array, beg, right);
return beg;
}
private static void swap(int[] array, int beg, int end) {
int tmp = array[beg];
array[beg] = array[end];
array[end] = tmp;
}
}
(三)、算法复杂度
时间复杂度 | 空间复杂度 | ||
---|---|---|---|
最好 | 平均 | 最好 | 平均 |
O(n) | O(n*log(n)) | O(n) | O(log(n)) |
(四)、优化
1.优化基准值的取法,三个元素取中间元素(最左侧元素,中间位置元素,最右侧元素,取中间值作为基准值,把确认的基准值交换到数组末尾或者开头,为了后面的整理动作做铺垫);
2.当区间已经比较小的时候再去递归其实效率就不高了,不在继续递归,而是直接进行插入排序;
3,如果区间特别大,递归深度也会非常深,当递归深度达到一定程度的时候,把当前区间的排序使用堆排序。
七,归并排序
归并排序可以用于外部排序,也可以适用于链表排序
(一)、思路
基于递归的方式,把待排序区间分成均等的两份,如果两个区间是有序区间,就可以按照把两个有序数组合并成一个有序数组来进行合并,如果当前分出的区间不是有序的,继续往下分割,如果当前区间中只有一个元素就一定是有序了。
(二)、代码
1.递归写法
public static void merge(int[] array, int low, int mid, int high) {
int[] output = new int[high - low];
int outputIndex = 0;
int cur1 = low;
int cur2 = mid;
while (cur1 < mid && cur2 < high) {
// 小的先放在 output 数组中
if (array[cur1] <= array[cur2]) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
} else {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
}
// 当上面循环结束的时候,肯定是 cur1 或者 cur2有一个先达到末尾,另一个还剩下一些内容
// 把剩下的内容拷贝到 output 中
while (cur1 < mid) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
}
while (cur2 < high) {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
for (int i = 0; i < high - low; i++) {
array[low + i] = output[i];
}
}
public static void mergeSort(int[] array) {
mergeSortHelper(array, 0, array.length);
}
// 这里使用的是前闭后开区间
private static void mergeSortHelper(int[] array, int low, int high) {
if (high - low <= 1) {
// 两者差值小于等于1表示区间只有 0 个元素或者1个元素
return;
}
int mid = (low + high) / 2;
mergeSortHelper(array, low, mid);
mergeSortHelper(array, mid, high);
merge(array, low, mid, high);
}
2,非递归写法
public static void mergeSortByLoop(int[] array){
for (int gap = 1; gap <array.length ; gap*=2) {
for (int i = 0; i < array.length; i+=2*gap) {
int beg = i;
int mid = i + gap;
int end = i + 2*gap;
if (mid > array.length){
mid = array.length;
}
if (end > array.length){
end = array.length;
}
merge(array,beg,mid,end);
}
}
}
public static void merge(int[] array, int low, int mid, int high) {
int[] output = new int[high - low];
int outputIndex = 0;
int cur1 = low;
int cur2 = mid;
while (cur1 < mid && cur2 < high) {
// 小的先放在 output 数组中
if (array[cur1] <= array[cur2]) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
} else {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
}
// 当上面循环结束的时候,肯定是 cur1 或者 cur2有一个先达到末尾,另一个还剩下一些内容
// 把剩下的内容拷贝到 output 中
while (cur1 < mid) {
output[outputIndex] = array[cur1];
outputIndex++;
cur1++;
}
while (cur2 < high) {
output[outputIndex] = array[cur2];
outputIndex++;
cur2++;
}
for (int i = 0; i < high - low; i++) {
array[low + i] = output[i];
}
}
(三)、算法复杂度
时间复杂度 | 空间复杂度 |
---|---|
O(n*log(n)) | O(n) |
稳定排序