前言
重新开始学算法,虽然已经上过 数据结构与算法 和 算法分析设计 的课程。
以后关于算法的代码都会放在算法代码仓库
交换两变量的值
第一种方法也是最常用的,没什么限制。借助一个辅助变量
1 2 3 4 5
| private static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; }
|
第二种方法,利用异或运算实现。不借助额外空间
1 2 3 4 5
| private static void swap_1(int[] array, int i, int j) { array[i] = array[i] ^ array[j]; array[j] = array[i] ^ array[j]; array[i] = array[i] ^ array[j]; }
|
异或运算 也可以叫做无进位相加(同为0,不同为1)
满足交换律和结合律
必须保证交换的变量内存地址不一致,否则两变量都会变为0。
交换的原理:
1 2 3
| a=甲^乙 b=乙 a=甲^乙 b=甲^乙^乙=甲^0=甲 a=甲^乙^甲=乙^0=乙 b=甲
|
异或运算还可以用来消除出现偶数次的值
选择排序
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
时间复杂度 O(n^2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public static int[] selectionSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); for (int i = 0; i < arr.length - 1; i++) { int temp = i; for (int j = i + 1; j < arr.length; j++) { if (arr[j] < arr[temp]) { temp = j; } } if (i != temp) { swap(arr, i, temp); } } return arr; }
|
冒泡排序
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
时间复杂度 O(n^2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static int[] bubbleSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); for (int i = 0; i < array.length - 1; i++) { boolean flag = true; for (int j = 0; j < array.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); flag = false; } } if (flag) { break; } } return arr; }
|
插入排序
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
时间复杂度 O(n^2)
1 2 3 4 5 6 7 8 9 10 11
| public static int[] insertionSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); for (int i = 1; i < arr.length; i++) { for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } } return arr; }
|
希尔排序(插入排序的改进)
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录”基本有序”时,再对全体记录进行依次直接插入排序。
时间复杂度 O(nlog2n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static int[] shellSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int temp; for (int step = arr.length / 2; step >= 1; step /= 2) { for (int i = step; i < arr.length; i++) { temp = arr[i]; int j = i - step; while (j >= 0 && arr[j] > temp) { arr[j + step] = arr[j]; j -= step; } arr[j + step] = temp; } } return arr; }
|
归并排序
归并排序
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
时间复杂度 O(nlogn)
空间复杂度 O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public static int[] mergeSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int[] tempArray = new int[array.length]; merge(arr, tempArray, 0, array.length - 1); return arr; }
private static void merge(int[] array, int[] tempArray, int start, int end) { if (start >= end) { return; } int mid = start + ((end - start) >> 2); int start1 = start; int end1 = mid; int start2 = mid + 1; int end2 = end; merge(array, tempArray, start1, end1); merge(array, tempArray, start2, end2); int temp = start; while (start1 <= end1 && start2 <= end2) { tempArray[temp++] = array[start1] < array[start2] ? array[start1++] : array[start2++]; } while (start1 <= end1) { tempArray[temp++] = array[start1++]; } while (start2 <= end2) { tempArray[temp++] = array[start2++]; } for (temp = start; temp <= end; temp++) { array[temp] = tempArray[temp]; } }
|
归并排序拓展
逆序对问题
在一个数组中,每一个数右边比当前数小的数,与这个数组成一个逆序对。
如数组 1,3,4,2,5
逆序对为 3,2 4,2
即求 右边有多少个数比当前数小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public static int reverse(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int[] tempArray = new int[arr.length]; return mergeReverse(arr, tempArray, 0, arr.length - 1); }
private static int mergeReverse(int[] array, int[] tempArray, int start, int end) { if (start >= end) { return 0; } int mid = start + ((end - start) >> 2); int start1 = start; int end1 = mid; int start2 = mid + 1; int end2 = end; int result = 0; int temp = start; result += mergeReverse(array, tempArray, start1, end1); result += mergeReverse(array, tempArray, start2, end2); while (start1 <= end1 && start2 <= end2) { result += array[start1] <= array[start2] ? 0 : (end2 - start2 + 1); tempArray[temp++] = array[start1] > array[start2] ? array[start1++] : array[start2++]; } while (start1 <= end1) { tempArray[temp++] = array[start1++]; } while (start2 <= end2) { tempArray[temp++] = array[start2++]; } for (temp = start; temp <= end; temp++) { array[temp] = tempArray[temp]; } System.out.println(Arrays.toString(tempArray)); return result; }
|
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
如数组 1,3,4,2,5
小和为 1 + 1+3 + 1 + 1+3+4+2 = 16
即求 右边有多少个数比当前数大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public static int smallSum(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int[] tempArray = new int[arr.length]; return mergeSmallSum(arr, tempArray, 0, arr.length - 1); }
private static int mergeSmallSum(int[] array, int[] tempArray, int start, int end) { if (start >= end) { return 0; } int mid = start + ((end - start) >> 2); int start1 = start; int end1 = mid; int start2 = mid + 1; int end2 = end; int result = 0; int temp = start; result += mergeSmallSum(array, tempArray, start1, end1); result += mergeSmallSum(array, tempArray, start2, end2); while (start1 <= end1 && start2 <= end2) { result += array[start1] < array[start2] ? (end2 - start2 + 1) * array[start1] : 0; tempArray[temp++] = array[start1] < array[start2] ? array[start1++] : array[start2++]; } while (start1 <= end1) { tempArray[temp++] = array[start1++]; } while (start2 <= end2) { tempArray[temp++] = array[start2++]; } for (temp = start; temp <= end; temp++) { array[temp] = tempArray[temp]; } return result; }
|
快速排序
荷兰国旗问题(前置)
给定一个整数数组,给定一个值K,这个值在原数组中一定存在
要求把数组中小于K的元素放到数组的左边,大于K的元素放到数组的右边,等于K的元素放到数组的中间
做法是用两个数组下标作为边界,将数组分成三个区域,左边是小于k的元素,中间是等于k的元素,右边是大于k的元素
不断将元素与边界交换,实现划分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static int[] partition(int[] array, int key) { int[] arr = Arrays.copyOf(array, array.length); int l = -1, r = arr.length, i = 0; while (i < r) { if (arr[i] < key) { swap(arr, ++l, i++); } else if (arr[i] > key) { swap(arr, --r, i); } else { i++; } } return arr; }
|
快速排序
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
类似荷兰国旗问题
时间复杂度 O(n^2)
空间复杂度 O(logn)
但它的平摊期望时间是O(nlongn),而且隐含的常数因子很小,比归并小很多。所以对绝大多数顺序性较弱的随机数列来说,快排优于归并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| public static int[] quickSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); return quickSortProcess(arr, 0, arr.length - 1); }
private static int[] quickSortProcess(int[] arr, int l, int r) { if (l < r) { int partitionIndex = partition(arr, l, r); quickSortProcess(arr, l, partitionIndex - 1); quickSortProcess(arr, partitionIndex + 1, r); } return arr; }
private static int partition(int[] arr, int l, int r) { int pivot = l; int index = pivot + 1; for (int i = index; i <= r; i++) { if (arr[i] < arr[pivot]) { swap(arr, i, index); index++; } } swap(arr, pivot, index - 1); return index - 1; }
private static int[] quickSortProcessOptimization(int[] arr, int l, int r) { if (l < r) { int[] partitionIndex = partitionOptimization(arr, l, r); partitionOptimization(arr, l, partitionIndex[0]); partitionOptimization(arr, partitionIndex[1], r); } return arr; }
private static int[] partitionOptimization(int[] arr, int l, int r) { int pivot = l; r++; int i = l + 1; while (i < r) { if (arr[i] < arr[pivot]) { swap(arr, ++l, i++); } else if (arr[i] > arr[pivot]) { swap(arr, --r, i); } else { i++; } } swap(arr, pivot, l--); return new int[]{l, r}; }
|
堆排序
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
时间复杂度 O(nlogn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public static int[] heapSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int len = arr.length;
for (int i = len / 2; i >= 0; i--) { heapify(arr, i, len); }
for (int i = len - 1; i > 0; i--) { swap(arr, 0, i); len--; heapify(arr, 0, len); } return arr; }
private static void heapify(int[] arr, int i, int len) { int left = 2 * i + 1; int right = 2 * i + 2; int largest = i; if (left < len && arr[left] > arr[largest]) { largest = left; } if (right < len && arr[right] > arr[largest]) { largest = right; } if (largest != i) { swap(arr, i, largest); heapify(arr, largest, len); } }
|
基数排序
一种非比较型整数排序算法
其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
时间复杂度 O(k*n)
空间复杂度 O(k+n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public static int[] radixSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int max = arr[0]; for (int j : arr) { if (max < j) { max = j; } } int maxDigit = 0; if (max == 0) { maxDigit = 1; } else { for (int i = max; i != 0; i /= 10) { maxDigit++; } } int mod = 10; int dev = 1; for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) { int[][] counter = new int[mod * 2][0]; for (int k : arr) { int bucket = ((k % mod) / dev) + mod; counter[bucket] = arrayAppend(counter[bucket], k); } int pos = 0; for (int[] bucket : counter) { for (int value : bucket) { arr[pos++] = value; } } } return arr; }
private static int[] arrayAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; }
|
计数排序
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
时间复杂度 O(n+k)
空间复杂度 O(k)
空间换时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public static int[] countingSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int min = arr[0]; int max = arr[0]; for (int value : arr) { if (min > value) { min = value; } if (max < value) { max = value; } } int difference = 0; if (min < 0) { difference = -min; } for (int i = 0; i < arr.length; i++) { arr[i] += difference; } int[] bucket = new int[max + difference + 1]; for (int value : arr) { bucket[value]++; } int socketIndex = 0; for (int i = 0; i < bucket.length; i++) { while (bucket[i] > 0) { arr[socketIndex++] = i - difference; bucket[i]--; } } return arr; }
|
桶排序(计数排序的升级)
利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
在额外空间充足的情况下,尽量增大桶的数量
使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
时间复杂度 O(n+k)
空间复杂度 O(n*k)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public static int[] bucketSort(int[] array) { int[] arr = Arrays.copyOf(array, array.length); int min = arr[0]; int max = arr[0]; for (int value : arr) { if (min > value) { min = value; } if (max < value) { max = value; } } int bucketSize = 5; int bucketCount = (max - min) / bucketSize + 1; int[][] buckets = new int[bucketCount][0]; for (int j : arr) { int index = (j - min) / bucketSize; buckets[index] = arrayAppend(buckets[index], j); } int arrIndex = 0; for (int[] bucket : buckets) { if (bucket.length <= 0) { continue; } bucket = bubbleSort(bucket); for (int value : bucket) { arr[arrIndex++] = value; } } return arr; }
|
对数器
对数器(通过用大量测试数据来验证算法是否正确的一种方式):
1.有一个你想要测的方法a;
2.实现一个绝对正确但是复杂度不好的方法b;
3.实现一个随机样本产生器;
4.实现对比算法a和b的方法;
5.把方法a和方法b比对多次来验证方法a是否正确;
6.如果有一个样本使得比对出错,打印样本分析是哪个方法出错;
7.当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
这里附上一个对数器
以Java提供的数组排序作为参照,以检验算法的正确性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public void sortTest() { int[] a = new int[1000];
for (int i = 0; i < 1000; i++) { a[i] = (int) (-1000 * Math.random() + 500); } System.out.println(Arrays.toString(Sort.insertionSort(a))); System.out.println(Arrays.toString(Sort.bubbleSort(a))); System.out.println(Arrays.toString(Sort.selectionSort(a))); System.out.println(Arrays.toString(Sort.mergeSort(a))); System.out.println(Arrays.toString(Sort.quickSort(a))); System.out.println(Arrays.toString(Sort.heapSort(a))); System.out.println(Arrays.toString(Sort.radixSort(a))); System.out.println(Arrays.toString(Sort.countingSort(a))); System.out.println(Arrays.toString(Sort.bucketSort(a))); System.out.println(Arrays.toString(Sort.shellSort(a)));
Arrays.sort(a); System.out.println(Arrays.toString(a)); }
|
总结
参考:
菜鸟教程 ,有更详细的解释以及各种编程语言对各个算法的实现。
关于桶排序
基数排序与计数排序、桶排序这三种排序算法都利用了桶的概念,但对桶的使用方式不同
基数排序:根据键值的每位数字来分配桶;
计数排序:每个桶只存储单一键值;
桶排序:每个桶存储一定范围的数值;
关于算法稳定性
排序算法的稳定性
同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定性的;否则就没有。
不具备稳定性的排序:
选择排序、快速排序、堆排序、希尔排序
具备稳定性的排序:
冒泡排序、插入排序、归并排序、一切桶排序思想下的排序
各个算法时间复杂度、空间复杂度和稳定性:
排序算法 |
平均时间复杂度 |
辅助空间 |
稳定性 |
选择排序 |
O(n^2) |
O(1) |
不稳定 |
冒泡排序 |
O(n^2) |
O(1) |
稳定 |
插入排序 |
O(n^2) |
O(1) |
稳定 |
希尔排序 |
O(nlogn) |
O(nlogn) |
不稳定 |
归并排序 |
O(nlogn) |
O(n) |
稳定 |
快速排序 |
O(nlogn) |
O(nlogn) |
不稳定 |
堆排序 |
O(nlogn) |
O(1) |
不稳定 |
基数排序 |
O(n*k) |
O(n+k) |
稳定 |
计数排序 |
O(n+k) |
O(n+k) |
稳定 |
桶排序 |
O(n+k) |
O(n+k) |
稳定 |
目前没有找到时间复杂度 0(n1ogn) ,额外空间复杂度0(1),又稳定的排序。(鱼和熊掌不可兼得)
基于比较的排序,时间复杂度至少 O(nlogn)
稳定的排序,空间复杂度至少 O(n)
综合排序
综合排序即将不同排序的优势结合在一起,以实现不同情况下更加高效的排序。