排序的相关介绍:
所谓的排序就是使一串记录,按照其中的某个或者某些关键字的大小,递增或递减的排列起来的操作。平时的上下文中如果提到排序,通常指的是升序(非降序)。通常意义上的排序都是指的原地排序
稳定性:两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。
1.直接插入排序:
将区间分为有序区间和无序区间。前半部分为有序区间,后半部分为无序区间。
每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入。
实现代码:
//插入排序
public static void insertSort(int[] array) {
//bound 变量来把整个数组分成两个区间
//[0, bound) 已排序区间
//[bound, size) 待排序区间
for (int bound = 1; bound < array.length; bound++) {
//bound 下标对应的元素就是待插入元素
//把这个元素放到前面的有序顺序表中的合适位置
int tmp = array[bound];
int cur = bound - 1;
for (; cur >= 0; cur--) {
if (array[cur] > tmp) {
array[cur + 1] = array[cur];
} else {
break;
}
}
array[cur + 1] = tmp;
}
}
性能分析:
稳定性:稳定
插入排序的核心操作:拿到当前元素,把当前元素插入到前方的有序顺序表的合适位置
插入排序的两个重要特点:
(1)如果当前这个序列很短,那么插入排序很高效
(2)如果当前这个序列基本有序,那么插入排序效率也很高
2.希尔排序:基本思想是先选定一个整数n,把待排序文件中所有元素分成n个组,所有距离为n的元素分在同一个组,并对每一组内的记录进行排序,并按照一定规律减小 n。然后,取,重复上述分组和排序的工作。当n等于1时。所有元素再同一组内排好序。
希尔排序:gap是两个元素之间下标差值,也是分成的组的数量。
代码实现:
//希尔排序
public static void shellSort(int[] array) {
int gap = array.length;
while (gap > 1) {
insertSortGap(array, gap);
gap = gap / 2;
}
insertSortGap(array, 1);
}
private static void insertSortGap(int[] array, int gap) {
for (int bound = 1; bound < array.length; bound++) {
int tmp = array[bound];
int cur = bound - gap;
//同组之内的相邻元素之间下标差了 gap, 同组内元素进行比较。
for (; cur >= 0; cur -= gap) {
if (array[cur] > tmp) {
array[cur + gap] = array[cur];
} else {
break;
}
}
array[cur + gap] = tmp;
}
}
性能分析:
稳定性:不稳定
实际的希尔排序的实现序列选择很讲究,选择的好效率会很高。
3.直接选择排序:每一次从无序区间选出最大(或者最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素排完。
代码实现:
//直接选择排序
public static void selectSort(int[] array) {
//先创建一个 bound 变量,表示边界
//[0,bound) 已排序区间
//[bound, size) 待排序区间
for (int bound = 0; bound < array.length; bound++) {
//使用打擂台的方式找到待排序区间中的最小值
//bound 位置的元素就是擂台
for (int cur = bound + 1; cur < array.length; cur++) {
if (array[cur] < array[bound]) {
//打败擂主
swap(array, cur, bound);
}
}
}
}
private static void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
性能分析:
稳定性:不稳定
4.堆排序:基本原理也是基于选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大数。
注意:排升序要建立大堆;排降序要建立小堆
以升序为例:首先建立一个将待排序区间建成大堆,然后将待排序区间的第一个元素与最后一个元素交换,待排序区间减少一个元素,已排序区间增加一个元素,然后重复上述工作,直到排序完成。
代码实现:
//堆排序
public static void heapSort(int[] array) {
//1.创建堆
createHeap(array);
//2.循环取出堆顶的最大值,放到最后
for (int i = 0; i < array.length; i++) {
//待排序区间:[0, array.length - i)
//已排序区间:[array.length - i, array.length)
swap(array, 0, array.length - 1 - i);
//第一个参数树是数组,第二个参数是数组中有效元素的个数,
//第三个参数是从哪个位置进行调整
shiftDown(array, array.length - 1 - i , 0);
}
}
private static void createHeap(int[] array) {
//从最后一个非叶子结点出发,从后往前向下调整
for (int i = (array.length - 1 - 1)/ 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
private static void shiftDown(int[] array, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size && array[child + 1] > array[child]) {
child = child + 1;
}
if (array[child] > array[parent]) {
swap(array,parent, child);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
性能分析:
稳定性:不稳定
5.冒泡排序:在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序。
代码实现:
//冒泡排序
public static void bubbleSort(int[] array) {
//[0, bound)已排序区间
//[bound,array.length)待排序区间
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1] > array[cur]) {
swap(array,cur - 1, cur);
}
}
}
}
性能分析:
稳定性:稳定
6.快速排序:
(1)从待排序区间选择一个数作为基准
(2)Partition:遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边
(3)对左右两个小区间按照同样的方式处理,直到小区间的长度等于 1,代表已经有序,或者小区间的长度等于 0,代表没有数据
代码实现:两种方法
(1)方法一:
//快速排序
public static void quickSort(int[] array) {
//参数的含义表示针对数组中的哪段区间进行快速排序
//[0, array.lengt - 1]
quickSortHelp(array, 0, array.length - 1);
}
private static void quickSortHelp(int[] array, int left, int right) {
if (left >= right) {
//如果只有一个元素或者没有元素,都不需要排序
return;
}
//这个方法就是刚才进行区间整理的方法
//选取基准值,并且把小于基准值的放到左侧,大于基准值的放到右侧
//返回值[left, right] 最终整理完毕后,基准值的下标
int index = partition(array, left, right);
quickSortHelp(array, left, index - 1);
quickSortHelp(array, index + 1, right);
}
//基准值为最后一元素
private static int partition(int[] array, int left, int right) {
//基准值
int baseValueIndex = right;
int baseValue = array[right];
while (left < right) {
while (left < right && array[left] <= baseValue) {
left++;
}
//循环结束之后,left 指向的位置就是从左往右第一个比基准值大的元素
while (left < right && array[right] >= baseValue) {
right--;
}
//这个循环结束之后,right 指向的位置就是从右往左第一个比基准值小的元素
//交换 left 和 right 位置的元素
swap(array,left, right);
}
//为什么证明此处的 left 比基准值大呢?
//循环结束有两种情况
//1.left++ 导致的循环结束
// 上次循环过程中进行了一个 swap 操作,经过这个 swap 操作之后,right 一定指向一个大于基准值的元素,
// 此时如果 left 和 right 重合,那么 left 也指向在 right 位置处的那个大于基准值的元素。
//2.right-- 导致的循环结束
// 此时由于 left 刚刚找到一个比基准值大的元素,此时 left 与 right 重合之后,
// 对应的元素也就是刚才那个比基准值大的元素。
swap(array, left, baseValueIndex);
return left;
}
(2)方法二:
//非递归版本的快速排序
public static void quickSortByLoop(int[] array) {
//1.先创建一个栈,栈里面存的是待处理区间的下标
Stack<Integer> stack = new Stack<>();
//2.初始情况下待处理区间就是整个数组
stack.push(array.length - 1);
stack.push(0);
while (!stack.isEmpty()) {
//3.取栈顶元素,栈顶元素就是我们要处理的区间
int left = stack.pop();
int right = stack.pop();
if (left >= right) {
continue;
}
//4.对当前待处理区间进行整理
int index = partition(array, left, right);
//5.接下来要处理的区间要入栈
//[left, index - 1]
//[index + 1, right]
stack.push(index - 1);
stack.push(left);
stack.push(right);
stack.push(index + 1);
}
}
性能分析:
稳定性:不稳定
快速排序的缺点:
(1)如果当前逆序,表现不佳
(2)如果基准值选的不好,也会影响性能
(3)如果元素数量非常多,就会导致递归深度过大,有可能栈就存不下了
优化手段:
(1)优化取基准值的方式(取三个元素中第二大的元素):(第一个元素,最后一个元素,中间位置的元素)
(2)如果递归深度达到一定深度之后,就不再进行递归了,而是对当前的待排序区间直接使用其他排序算法(比如:堆排序).
(3)如果当前待排序区间已经比较小了(left 与 right 之间的差比较小),直接使用插入排序即可。
7.归并排序:建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将有序的子序合并,得到完全有序的序列;即先使每个子序有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
代码实现:两种方法
(1)方法一:
//归并排序
public static void mergeSort(int[] array) {
//后两个参数表示要进行归并排序的区间
//[0,array.length)
mergeSortHelp(array, 0, array.length);
}
private static void mergeSortHelp(int[] array, int left, int right) {
//[left, right) 构成了要去进行排序的区间
if (left >= right || right - left == 1) {
//空区间或者期间只有一个元素,都不需要进行归并排序
return;
}
//使用一种类似于后序遍历的方式
//先把当前的待排序区间拆分成两半,
//先递归的对这两个子区间进行归并排序,保证两个区间有序之后,再进行合并
int mid = (left + right) / 2;
//[left, mid)
//[mid, right)
mergeSortHelp(array, left, mid);
mergeSortHelp(array, mid, right);
merge(array, left, mid, right);
}
private static void merge(int[] array, int left, int mid, int right) {
//创建一段临时空间辅助进行归并
//这个临时空间的长度应该是两个带归并区间的长度之和
int length = right - left;
int[] output = new int[length];
//这个变量保存着当前 output 中的末尾元素的下标
int outputIndex = 0;
//i 和 j 是用来遍历两个区间的辅助变量
//[left, mid)
//[mid, right)
int i = left;
int j = mid;
while (i < mid && j < right) {
//此处的 if 条件必须要 <= ,否则无法保证稳定性
if (array[i] <= array[j]) {
//i 对用的元素比 j 小
//就把 i 对用的元素插入到 output 末尾
output[outputIndex++] = array[i++];
} else {
output[outputIndex++] = array[j++];
}
}
//上面的循环结束之后,两个区间至少有一个是遍历完了的,
//就把剩下的区间的内容直接拷贝到 output 中即可。
while (i < mid) {
output[outputIndex++] = array[i++];
}
while (j < right) {
output[outputIndex++] = array[j++];
}
//最后一步把 output 中的元素拷贝回原来的区间
for (int k = 0; k < length; k++) {
array[left + k] = output[k];
}
}
(2)方法二:
//归并排序(非递归)
public static void mergeSortByLoop(int[] array) {
//借助下标和相关的规律来进行分组
//初始情况下,每个元素单独作为一组
//[0] [1] [2] [3] [4] [5]
//[0, 1] 和[2, 3] 合并 [4, 5] 和 [6, 7]区间合并
for (int gap = 1; gap < array.length; gap *= 2) {
for (int i = 0; i < array.length; i += 2 * gap) {
//这个循环负责在 gap 为指定值的情况下把所有区间进行合并
//针对当前的 i ,也能划分出两个需要进行归并的区间
//[beg, mid)
//[mid, end)
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);
}
}
}
性能分析:
稳定性:稳定
归并排序的相关应用领域:
1.数据允许在外存中,外部排序的核心思路就是归并排序
2. 归并排序也是一种高效的给链表进行排序的算法。
3.也是各种标准库中稳定排序算法的主要实现方式