排序算法
均按照升序演示
排序算法的稳定性:如果相等的两个元素,在排序前后的相对位置保持不变,我们就称为是稳定的排序算法,否则是不稳定的。
原地算法 In-place:不依赖额外的资源或依赖少数的额外资源,仅依靠输出来覆盖输入,空间复杂度为O(1)的都可以认为是原地算法
1、冒泡排序(Bubble Sort)
时间复杂度:O(n^2) 空间复杂度O(1) 可以是稳定的排序算法
思想:
- 从头开始比较相邻的一对元素,如果左边的比右边的大,就交换两数位置
- 忽略到最大的元素,从剩下的元素中进行一边冒泡排序,直到使数组变为有序
代码:
// 冒泡
public void sort(){
for (int end = array.length - 1; end > 0; end--){
for (int begin = 1; begin <= end; begin++){
if (cmpIndex(begin, begin-1) < 0){
swap(begin, begin-1);
}
}
}
}
改进优化1
引入标记flag,在内层循环中,如果发现没有进入if子句,就说明该数组已经排好了,直接break即可
public void sort() {
for (int end = array.length - 1; end > 0; end--) {
boolean flag = true;
for (int begin = 1; begin <= end; begin++){
if (cmpIndex(begin, begin - 1) < 0){
flag = false;
swap(begin, begin - 1);
}
}
if (flag) break;
}
}
改进优化2
如果序列尾部已经局部有序,可以记录最后一次交换的位置,减少比较次数
@Override
public void sort() {
for (int end = array.length - 1; end > 0; end--){
int sortIndex = 1;
for (int begin = 1; begin <= end; begin++){
if (cmpIndex(begin, begin - 1) < 0){
swap(begin, begin - 1);
sortIndex = begin;
}
}
end = sortIndex;
}
}
2、选择排序(Select Sort)
时间复杂度:O(n^2) 空间复杂度O(1) 不稳定的排序算法
思想:从序列中找出最大的元素,然后和最末尾的元素交换位置。或者是从序列中找出最小的元素和第一个交换位置
代码
public static void selectSort(int[] nums){
for (int i = 0; i < nums.length - 1; i++){
int target = i;
int min = nums[i];
for (int j = i; j < nums.length; j++){
if (nums[j] < min) { // 遍历后面的数找到最小值
min = nums[j];
target = j;
}
}
// 将nums[i] 和 nums[j] 交换一下即可
int temp = nums[i];
nums[i] = nums[target];
nums[target] = temp;
}
}
3、堆排序(Heap Sort)
可以认为是对选择排序的一种优化,因为它在内层循环中挑选最大值的时候时间复杂度较低
堆:
- 必须是一棵完全二叉树
- 对于堆里面的值,父节点必须大于子节点
思想:
- 对序列进行原地建堆(heapify)O(n)
- 重复以下操作,直到堆的元素数量为1 O(n)
- 交换堆顶元素与尾元素
- 堆的元素数量减一
- 对0号位置进行一次shitdown操作 O(log(n))
4、快速排序(Quick Sort)
逐渐地将每一个元素转化为轴点元素
思想:通过一趟排序将要排序的数据分割为两部分,其中一部分的所有数据都要比另外一部分的所有数据要小,然后再按照此方法对两部分数据分别进行快速排序,使整个数据变为有序数据
- 选定Pivot中心轴
- 将大于Pivot的数字放在Pivot右边
- 将小于Pivot的数字放在Pivot左边
- 分别对左右子序列重复前三步操作
实现方式: 左右指针
@Override
public void sort(){
quickSort(array, 0, array.length - 1);
}
public void quickSort(Integer[] array, int left, int right){
int pivot = array[left]; // array[0]作为中心轴
int begin = left;
int end = right;
while (left < right){
while (left < right){
if (cmpElements(pivot, array[right]) < 0){ // 右边元素大于pivot
right--;
}else { // 右边元素小于等于pivot
array[left] = array[right];
left++;
break;
}
}
while (left < right){
if (cmpElements(pivot, array[left]) > 0){ // 左边元素小于轴点
left++;
} else { // 左边元素大于等于轴点
array[right] = array[left];
right--;
break;
}
}
}
array[left] = pivot;
if (cmpElements(left, begin+1) > 0){
quickSort(array, begin, left - 1);
}
if (cmpElements(end, left + 1) > 0){
quickSort(array, left + 1, end);
}
}
除次之外,用快慢指针的方式也可以实现,关键在于一遍遍历找到左右两部分数据这个操作
5、插入排序(Insertion Sort)
类似于扑克牌排序
思想:
- 在执行的过程中,插入排序会将序列分为两部分头部和尾部(头部是已经拍好序的,尾部是待排序的)
- 从头开始扫描每一个元素,每当扫描到一个元素,就将它插到头部合适的位置,使头部数据依然有序
public void sort() {
// begin 代表你起的牌的索引(这里直接从第二张牌开始), 要插到前面合适的位置
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
while(cmpIndex(cur, cur - 1) < 0){
swap(cur, cur - 1);
if (--cur <= 0) break;
}
}
}
逆序对:
- 数组<2, 3, 8, 6, 1> 的逆序对是<2,1> ❤️,1> <8,6> <8,1> <6,1>
- 插入排序的时间复杂度与逆序对的数量成正比
插入排序的优化算法:(将交换转为挪动)
- 先将待插入的元素备份
- 头部有序数据中比待插元素大的,朝尾部方向挪动一个位置
- 将待插元素备份放到空出的位置
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
int val = array[begin];
while (cmpIndex(cur, cur - 1) < 0){
array[cur] = array[cur-1];
if (--cur <= 0) break;
}
array[cur] = val;
}
插入排序的优化算法:(二分搜索):
-
在插入元素v的过程中,可以先二分搜索出合适的插入位置,然后在进行插入,优化了比较次数,但挪动没有办法优化
@Override public void sort() { for (int begin = 1; begin < array.length; begin++) { // 要插入的值 int val = array[begin]; // 要插入的位置 int position = search(begin); int cur = begin; // 移动元素, 索引大的先移动 for (;cur > position;cur--){ array[cur] = array[cur-1]; } array[position] = val; } } //供插入排序使用的二分搜索, 返回要插入元素的待插入位置索引 // 返回的位置是从左到右第一个大于target的元素的位置索引 private int search(int index){ int begin = 0; int end = index; while (begin < end){ int mid = (begin + end) >> 1; if (cmpIndex(index, mid) < 0){ end = mid; }else { begin = mid + 1; } } return begin; }
6、希尔排序(Shell Sort)
相当于改进的插入排序。
思想: 是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。 Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数
@Override
public void sort() {
List<Integer> integers = shellStepSequece();
// 对每个步长进行选择排序
for (Integer integer : integers) {
sort(integer);
}
}
public void sort(int step){
for (int col = 0; col < step; col++) {
// 对每列进行选择排序
// 插入排序
for (int begin = col + step; begin < array.length; begin += step){
int cur = begin;
while (cur > col && cmpIndex(cur, cur - step) < 0){
swap(cur, cur - step);
cur -= step;
}
}
}
}
// 获取步长序列, 这里的序列是2以及2的倍数,是shell推荐的
private List<Integer> shellStepSequece(){
List<Integer> stepSequence = new ArrayList<>();
int step = array.length;
while ((step /= 2) > 0){
stepSequence.add(step);
}
return stepSequence;
}
二分搜索(Binary Search)
思路:
在一个排好序列的数组中,begin为0,end为length
- 假设我们在 [begin, end) 范围中搜索某个元素v,mid = (begin + end) / 2
- 如果等于m,则查找完成
- 如果小于v,则将end置为mid,在左半部分重复上面的操作
- 如果大于m,则将begin置为mid+1,在右半部分重复上面的操作
- 如果begin = end了,则查找失败
递归实现:
// 如果查找不到返回-1
public static int indexOf(int[] array, int begin, int end, int target){
if (array.length == 0 && array == null) return -1;
while (begin < end){
int mid = (begin + end) >> 1;
if (target == array[mid]) {
return mid;
}else if (target < array[mid] ){
return indexOf(array, begin, mid, target);
}else if (target > array[mid]){
return indexOf(array, mid + 1, end, target);
}
}
return -1;
}
public static int indexOf(int[] array, int target){
return indexOf(array, 0, array.length, target);
}
非递归实现:
public int indexOf2(int[] array, int target){
if (array.length == 0 && array == null) return -1;
int begin = 0;
int end = array.length;
while (begin < end){
int mid = (begin + end) >> 1;
if (target < array[mid]){
end = mid;
}else if (target > array[mid]){
begin = mid + 1;
}else {
return mid;
}
}
return -1;
}
7、归并排序(Merge Sort)
思想:
- 不断地将当前序列平均分割成两个子序列,分割到不能在分割为止(序列中只有一个元素)(divide)
- 不断将两个子序列按照大小顺序合并成一个有序序列,直到最终只剩下一个有序序列(merge)
public void sort() {
sort(0, array.length);
}
/**
* 对 [begin, end) 范围内的数据进行归并排序
* @param begin
* @param end
*/
private void sort(int begin, int end){
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 li = 0, le = mid - begin; // leftArray的索左右
int ri = mid, re = end; // array 右半部分的左右
int ai = begin; // array 左半部分的指针
// 备份左边数组
int[] leftArray = new int[mid - begin];
for (int i = 0; i < leftArray.length; i++) {
leftArray[i] = array[begin + i];
}
/*
* 这里和算法题目中不一样的是,就算先跳出循环,array中没有被赋值的地方也有原本的值
* 并且这些值正好满足排序,所以不用担心先break掉的情况
*/
while (li < le){
if (ri < re && leftArray[li] > array[ri]){
array[ai++] = array[ri++];
}else {
array[ai++] = leftArray[li++];
}
}
}
8、计数排序(Counting Sort)
适合对一定范围内的元素进行排序
思路: 创建一个基于要排序数组最大值的数组,其索引代表原数组的值,其值代表该索引所代表的值在原数组出现的次数,然后便利该数组将索引放回原数组中即可
实现
public void sort() {
// 先找到最大值
int max = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] > max) max = array[i];
}
// 创建最大值空间的数组,并将array填入
int[] arrayRef = new int[max+1];
for (int i = 0; i < array.length; i++) {
arrayRef[array[i]]++;
}
// 将数组遍历填入原数组中
int j = 0;
for (int i = 0; i < arrayRef.length; i++) {
while (arrayRef[i]-- > 0){
array[j++] = i;
}
}
}
上面的实现是最简单的实现,它只能排正整数,不稳定,并且及其浪费空间
改进方法:
public void sort() {
// 先找到最大值,最小值
int max = array[0];
int min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) max = array[i];
if (array[i] < min) min = array[i];
}
// 创建最大值空间的数组,并将array填入,累加的次数
int[] arrayRef = new int[max - min + 1];
for (int i = 0; i < array.length; i++) {
arrayRef[array[i] - min]++;
}
// 累加次数
for (int i = 1; i < arrayRef.length; i++) {
arrayRef[i] += arrayRef[i-1];
}
// 从后往前遍历,将之放到有序数组上,从后往前可以保证稳定性
Integer[] arrayCopy = array.clone();
for (int i = array.length-1; i >= 0 ; i--) {
array[--arrayRef[arrayCopy[i] - min]] = arrayCopy[i];
}
}
9、基数排序(Radix Sort)
适合整数排序(尤其是正整数)
思路: 依次对个位数,十位数,百位数…进行计数排序,排完即有序
@Override
public void sort() {
// 找最大值,确定位数,即要计数排序的次数
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) max = array[i];
}
/**
* max = 593
* 个位数:593 / 1 % 10 = 3
* 十位数:593 / 10 % 10 = 9
* 百位数:593 / 100 % 10 = 5
* 千位数:593 / 1000 % 10 = 0
*/
for (int divider = 1; divider <= max; divider *= 10) {
countingSort(divider);
}
}
// 适用于基数排序的计数排序
private void countingSort(int divider){
int[] arrayRef = new int[10];
for (int i = 0; i < array.length; i++) {
arrayRef[array[i] / divider % 10]++;
}
// 累加次数
for (int i = 1; i < arrayRef.length; i++) {
arrayRef[i] += arrayRef[i-1];
}
// 从后往前遍历,将之放到有序数组上
Integer[] arrayCopy = array.clone();
for (int i = array.length-1; i >= 0 ; i--) {
array[--arrayRef[arrayCopy[i] / divider % 10]] = arrayCopy[i];
}
}
10、桶排序(Bucket Sort)
思路:
- 创建一定数量的桶(数组,链表)
- 按照一定的规则(不同类型的数据规则不同,整数\小数),将桶中的元素均匀分配到对应的桶
- 分别对每个桶单独排序
- 将所有非空桶的元素合并成有序序列