1. 排序
(1)前提是存储结构是线性表(一般都是顺序表 ,甚至以数组为主)
(2)要求元素是具备比较大小的能力(Java中:基本类型中的数值、对象+Comparable、对象+Comprartor)
(3)原地(就地)排序:在给定数组的原有空间中完成排序,结果在原数组中体现即可。
(4)数据全在内存中的排序(数组+内存:取下标访问元素的时间复杂度可认为一定是O(1) )。
(5)排序代码中的关键:区间:数组+开始位置下标+结束位置下标,左闭右开[ )、左闭右闭[ ]。
(6)常见的排序算法
(7)O(n^2)级别的算法:冒泡排序、选择排序、插入排序
PS:减治算法(分治算法):
1)减:通过一个有限步骤,使得问题的规模变小。冒泡排序:冒泡步骤;选择排序:选择+交换;插入排序:查找+插入。把n个数的排序变为(n-c)个数的排序。
2)治:剩余的问题还是使用相同的方式进行解决。出口:待排序数组的长度<=1时。
2. 插入排序(Insert Sort)
(1)在有序区间中,找到合适的位置并插入。
(2)遍历方式
从后往前遍历有序区间,每个元素即为e,要插入的元素记为x(一定紧挨着有序区间,且在后面).遍历完后,要么e<=x,x应插入在e后边(紧挨着);要么整个区间都遍历完了,x应插入在第一个[0]。期间为保证插入x时位置存在,当e>x时,把e往后移给x腾地。
public static void insertSort(long[] arr){
for(int i=0;i<arr.length;i++){
long x=arr[i];
int j;
for(j=i-1;j>=0 && arr[j]>x;j--){
arr[j+1]=arr[j];
}
arr[j+1]=x;
}
(3)时间与空间复杂度
1)时间复杂度
最好:每个x都从小到大已经有序,O(n)
最坏:x比有序区间的最后一个元素还小(待排序的原数组是从大到小逆序排列),O(n^2)
平均:O(n^2)
2)空间复杂度
都是O(1),是一种稳定的排序算法。
3. 冒泡排序(Bubble Sort)
(1)冒泡排序:通过冒泡的过程,把所有无序区间的所有元素都消除掉(剩下一个就实际有序了)。
把整个数组看作前边是无序的、后边是有序的。
每次冒泡过程:在无序区间相邻的两个元素间比较,确保无序区间的所有元素都参加了比较。目标是当前无序区间的最大值冒泡到当前无序区间的最后一个位置。无序区间少了一个元素,有序区间多了一个元素。
整个冒泡排序有 n-1 次冒泡过程。
(2)冒泡过程中一旦发生了交换,说明存在相邻两元素之间 前边的元素>后边的元素。反过来说,单次冒泡过程,若一次交换都没有发生,说明不存在相邻两元素之间 前边的元素>后边的元素。
任意相邻的两个元素之间,前边的元素<=后边的元素。
冒泡过程只在无序区间进行。无序区间的任意相邻两个元素满足 前边的元素<=后边的元素。我们认为的无序区间是有序的。所以整个数组已经全是有序的。
public static void BubbleSort(long[] arr){
for(int i=0;i< arr.length-1;i++){//需要n-1次冒泡过程
boolean sorted=true;//每次冒泡过程,假设无序区间是有序的
for(int j=0;j< arr.length-i-1;j++){
if(arr[j]>arr[j+1]){//这里若加了=就事情了稳定性
swap(arr,j,j+1);
sorted=false;//说明假设无序区间是有序的不成立
}
}
if(sorted){//冒泡过程完成后,若sorted==true,说明一次交换过程都没发生过。
return;
}
}
}
(3)复杂度
1)时间复杂度
最好:O(n),当数组已经有序。
最坏:O(n^2),当数组是逆序的。
平均:O(n^2)
2)空间复杂度
都是O(1),具备稳定性。
PS:(24条消息) 排序算法——冒泡排序VS插入排序_LemmonTreelss的博客-CSDN博客_冒泡排序 插入排序
4. 选择排序(Select Sort)
(1)把整个待排序数组看作两个区间:无序区间+有序区间
1)[无序区间] [有序区间] 找到无序区间中最大元素所在的下标,然后将该元素放到无序区间最后。如:[5 3 2 4 2][6 7 8 9]--> [3 2 4 2][5 6 7 8 9]
2)[有序区间] [无序区间] 找到无序区间中最下元素所在的下标,然后将该元素放到无序区间开始。如:[0 1 2 3][9 4 5 8]-->[0 1 2 3 4][9 5 8]
(2)在无序区间中找到最大元素:暴力查找(遍历)
public static void SelectSort(long[] arr){
for(int i=0;i<arr.length-1;i++){
//无序区间+有序区间 --> [0,arr.length-i)[arr.length-i,arr.length)
int maxIdx=0;
for(int j=1;j<arr.length-i;j++){
if(arr[j]>arr[maxIdx])
maxIdx=j;
}
//无序区间的最大元素为arr[maxIdx],把它放到无序区间最后一个位置,无序区间最后一个元素位置下标是[arr.length-i-1]
swap(arr,maxIdx, arr.length-i-1);
}
}
public static void swap(long[] arr,int i,int j){
long temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
(3)在无序区间中找到最小元素
public static void SelectSort1(long[] arr){
for(int i=0;i< arr.length-1;i++){
//有序区间+无序区间--》找到无序区间中最小元素所在的下标,然后将该元素放到无序区间开始
int minIdx=i;
for(int j=i+1;j< arr.length;j++){
if(arr[j]<arr[minIdx])
minIdx=j;
}
swap(arr,minIdx,i);
}
}
(4)复杂度
1)时间复杂度
最好、最坏、平均:O(n^2)。
2)空间复杂度
都是O(1),但保证不了稳定性。
5. 希尔排序(Shell Sort)
(1)希尔排序
希尔排序是插入排序的升级版本。
从无序区间(带间隔的)中取第一个元素,去有序区间(带间隔的)进行查找并插入。
PS:插入排序,元素越趋于有序,效率越高。希尔排序,进行大量的分组插排。
(2)在这个过程中我们选择增量 gap=length/2 ,缩小增量以 gap=gap/2 的方式。
public static void ShellSort(long[] arr){
if(arr.length==0)
return;
int gap=arr.length/2;
while(true){
for(int i=gap;i<arr.length;i++){
long x=arr[i];
int j;
for(j=i-gap;j>=0 && arr[j]>x;j-=gap){
arr[j+gap]=arr[j];
}
arr[j+gap]=x;
}
if(gap==1)
break;
gap=gap/2;
}
}
PS:希尔排序相关: 百度安全验证https://baijiahao.baidu.com/s?id=1707870928646769729&wfr=spider&for=pc
6. 堆排序(Heap Sort)
选择排序的优化-->堆排序
[无序区间] [有序区间] 中,将无序区间维护成一个大堆(从小到大排序,需要大堆),利用大堆从无序区间中找到最大值,交换。
PS:可参考:http://t.csdn.cn/adOpq
7. 快速排序(Quick Sort)
(1)思想:对一个区间的元素进行排序,在这个区间中任选一个元素(一般挑区间最边上的元素--最右边的, pivot ),遍历整个待排序区间,将每个元素都和 pivot 作比较(<=或>=),同时把 pivot 放到合适的位置,要求所有(<= pivot )的元素都在 pivot 左边,所有(>= pivot )的元素都在 pivot 右边。
(2)基本框架
在待排序区间内,选择一个基准值 pivot ;
对待排序区间进行 partition 操作,目标是将待排序区间分开,左边的<= pivot ,右边的>= pivot ;
分别在对左右两个小区间按照相同方式进行处理,知道待排序区间元素个数 <=1 ,此时待排序区间就是有序的。
PS:分治算法:把一个大问题(n个元素的排序问题)经过一定的步骤(partition)分成多个(至少两个)相同的小规模问题,再按照相同方式继续处理小问题。
(3)具体 partition 的方式
PS:一定要确保 partition 的区间内的每个元素都和 pivot 做过比较 。
1)hoare 法:左右往中间靠
private static int partitionHoare(long[] array, int from, int to) {
long pivot = array[to]; // 选择区间的最右边的元素作为基准值
int left = from;
int right = to;
// 当 [left, right) 区间内还有元素时,循环,得继续
while (left < right) { // left < right 只在此处判断了
// 当选择最右边作为基准值时,优先从左边开始,否则,某些情况下会有元素没有和 pivot 比较过
while (left < right && array[left] <= pivot) {
left++; // left 一直在变,可能破坏 left < right 的约束
}
while (left < right && array[right] >= pivot) {
right--; // right 一直在变,可能破坏 left < right 的约束
}
// array[left] > pivot && array[right] < pivot
swap(array, left, right);
}
/* 真实中的快排不用写这些代码的,只是为了检查正确性约束 */
assertTrue(left == right, "left 和 right 必须相等");
assertTrue(left >= from && left <= to, "left 必须在 [from, to] 区间内");
// 断言 left == right
// right 的位置就是 pivot 应该所在的下标
// [right, to] 大于等于基准值
// 即使交换 [right] 和 [to] 也没有打破 [right, to] 大于等于基准值
swap(array, right, to);
// [from, left) 都是小于等于基准值的
for (int i = from; i < left; i++) {
assertTrue(array[i] <= pivot, "左边区间的元素必须都是小于等于基准值");
}
// [left + 1, to] 都是大于等于基准值的
for (int i = left + 1; i <= to; i++) {
assertTrue(array[i] >= pivot, "右边区间的元素都是大于等于基准值");
}
return right; // 最终返回基准值所在的下标
}
private static void swap(long[] array, int i, int j) {
long t = array[i];
array[i] = array[j];
array[j] = t;
}
private static void assertTrue(boolean condition, String message) {
if (!condition) {
throw new RuntimeException(message);
}
}
2)挖坑法:左右往中间靠
private static int partitionDigHole(long[] array, int from, int to) {
long pivot = array[to];
int left = from;
int right = to;
// 当 [left, right) 区间内还有元素时,循环,得继续
while (left < right) {
while (left < right && array[left] <= pivot) {
left++;
}
array[right] = array[left];
while (left < right && array[right] >= pivot) {
right--;
}
array[left] = array[right];
}
/* 真实中的快排不用写这些代码的,只是为了检查正确性约束 */
assertTrue(left == right, "left 和 right 必须相等");
assertTrue(left >= from && left <= to, "left 必须在 [from, to] 区间内");
// [from, left) 都是小于等于基准值的
for (int i = from; i < left; i++) {
assertTrue(array[i] <= pivot, "左边区间的元素必须都是小于等于基准值");
}
// [left + 1, to] 都是大于等于基准值的
for (int i = left + 1; i <= to; i++) {
assertTrue(array[i] >= pivot, "右边区间的元素都是大于等于基准值");
}
array[left] = pivot;
return left;
}
3)前后指针法:从前往后靠
private static int partition3(long[] array, int from, int to) {
/*
小于基准值的区间范围: [from, b)
大于等于基准值的区间范围:[b, d)
未比较的元素的区间范围: [d, to)
基准值 [to, to]
*/
long pivot = array[to];
// 最开始
// [from, b) => [from, from) 区间内一个元素都没有
// [b, d) => [from, from) 区间内一个元素都没有
// [d, to) => [from, to) 整个区间所有元素都在里面
int b = from;
for (int d = from; d < to; d++) {
if (array[d] < pivot) {
swap(array, b, d);
b++;
}
}
swap(array, b, to);
return b;
}
4)进阶版本:
public static void 进阶版Partition(long[] array, int from, int to) {
long pivot = array[to];
/*
[from, b) 小于基准值
[b, d) 等于基准值
[d, g] 未比较的元素
(g, to] 大于基准值
*/
int b = from;
int d = from;
int g = to;
// [d, g] 这个区间内没有元素时停止; d <= g 说明还有元素
while (d <= g) {
if (array[d] == pivot) {
d++;
} else if (array[d] < pivot) {
swap(array, d, b);
b++;
d++;
} else {
// array[d] > pivot
swap(array, d, g);
g--;
}
}
}
(4)复杂度
1)时间复杂度
最好:O(n*log(n)) ,共有n层,每层基本是O(n) 。
最坏:O(n^2)
平均:O(n*log(n))
2)空间复杂度
最好:O(log(n)),完全二叉树的高度
最坏:O(n),单枝树的高度
平均:O(log(n))
(5)快速排序的优化
1)处理数据量较小的排序时,插排的速度更优
2)更细致的选择基准值:再待排序区间中选择一个随机位置作为基准值(计算机生成随机数速度不快),再把选出来的基准值交换到最右边。如:
private static void quickSortInternal(long[] array, int from, int to) {
// 区间内的元素个数
int size = to - from + 1;
if (size <= 16) {
insertSort(array, from, to);
return;
}
// 三数取中法
long leftValue = array[from];
long rightValue = array[to];
int midIdx = from + (to - from) / 2;
long midValue = array[midIdx];
if (leftValue <= rightValue) {
if (rightValue <= midValue) {
// rightValue 作为基准值
} else if (leftValue <= midValue) {
// midValue
swap(array, midIdx, to);
} else {
// leftValue
swap(array, from, to);
}
} else {
// leftValue > rightValue
if (leftValue <= midValue) {
// leftValue
swap(array, from, to);
} else if (midValue <= rightValue) {
// rightValue
} else {
// midValue
swap(array, midIdx, to);
}
}
int i = partition(array, from, to);
quickSortInternal(array, from, i - 1);
quickSortInternal(array, i + 1, to);
}
private static int partition(long[] array, int from, int to) {
long pivot = array[to];
int left = from;
int right = to;
while (left < right) {
while (left < right && array[left] <= pivot) {
left++;
}
array[right] = array[left];
while (left < right && array[right] >= pivot) {
right--;
}
array[left] = array[right];
}
array[left] = pivot;
return left;
}
private static void swap(long[] array, int i, int j) {
long t = array[i];
array[i] = array[j];
array[j] = t;
}
private static void insertSort(long[] array, int from, int to) {
int size = to - from + 1;
for (int i = 1; i < size; i++) {
// 有序区间 [from, from + i)
// 无序区间的第一个元素的下标 [from + i]
long x = array[from + i];
// 遍历有序区间
int j;
for (j = from + i - 1; j >= from && array[j] > x; j--) {
array[j + 1] = array[j];
}
array[j + 1] = x;
}
}
(6)非递归实现快速排序
public static void quickSort非递归(long[] array) {
Deque<Integer> stack = new LinkedList<>();
// 一开始就把待排序区间放入
stack.push(0);
stack.push(array.length - 1);
while (!stack.isEmpty()) {
int to = stack.pop();
int from = stack.pop();
if (to - from + 1 <= 1) {
continue;
}
int i = partition(array, from, to);
stack.push(from);
stack.push(i - 1);
stack.push(i + 1);
stack.push(to);
}
}
private static int partition(long[] array, int from, int to) {
long pivot = array[to];
int left = from;
int right = to;
while (left < right) {
while (left < right && array[left] <= pivot) {
left++;
}
array[right] = array[left];
while (left < right && array[right] >= pivot) {
right--;
}
array[left] = array[right];
}
array[left] = pivot;
return left;
}
8. 归并排序(Merge Sort)
(1)复杂度
1)时间复杂度
最好、最坏、平均:O(n*log(n)) 。
2)空间复杂度
最好、最坏、平均:O(n),具备稳定性。
(2)实现
1)
public static void mergeSort(long[] array) {
mergeSortInternal(array, 0, array.length);
}
// 待排序区间:[from, to)
private static void mergeSortInternal(long[] array, int from, int to) {
if (to <= 1 + from) {
return;
}
// 把待排序区间对半分开;找到中间位置下标
int mid = from + (to - from) / 2;
// 整个区间被切割成, [from, mid), [mid, to),按照相同的方式,先把左右两个区间变得有序
mergeSortInternal(array, from, mid);
mergeSortInternal(array, mid, to);
// [from, mid) 有序了
// [mid, to) 有序了
merge(array, from, mid, to);
}
private static void merge(long[] array, int from, int mid, int to) {
int size = to - from;
// 需要的额外空间
long[] e = new long[size];
int eIdx = 0;
int leftIdx = from; // 左边区间要处理的元素的下标
int rightIdx = mid; // 右边区间要处理的元素的下标
// 什么条件表示左边区间 [from, mid) 就没有元素了 leftIdx == mid
// 同理右边区间:rightIdx == to
// 两个区间都有元素的时候才要比较
// 左区间有元素:leftIdx < mid
// 右: rightIdx < to
// leftIdx < mid && rightIdx < to
while (leftIdx < mid && rightIdx < to) {
// 要比较的两个元素 array[leftIdx] 和 array[rightIdx]
if (array[leftIdx] <= array[rightIdx]) {
// 取左边的元素放入 e 数组
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
} else {
// 取右边的元素放入 e 数组
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// 说明有一个区间的元素被全部取完了
if (leftIdx < mid) {
// 右边取完了,把左边的所有元素依次放到 e 中
while (leftIdx < mid) {
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
}
} else {
while (rightIdx < to) {
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// e 里是有序的 [0, size)
// 要把有序的元素从 e 中搬回到 array 中 [from, to)
for (int i = 0; i < size; i++) {
array[from + i] = e[i];
}
}
2)
public static void mergeSort2(long[] array) {
long[] e = new long[array.length];
mergeSortInternal2(array, 0, array.length, e);
}
private static void mergeSortInternal2(long[] array, int from, int to, long[] e) {
if (to <= 1 + from) {
return;
}
// 把待排序区间对半分开
// 找到中间位置下标
int mid = from + (to - from) / 2;
// 整个区间被切割成
// [from, mid)
// [mid, to)
// 按照相同的方式,先把左右两个区间变得有序
mergeSortInternal2(array, from, mid, e);
mergeSortInternal2(array, mid, to, e);
// [from, mid) 有序了
// [mid, to) 有序了
merge2(array, from, mid, to, e);
}
private static void merge2(long[] array, int from, int mid, int to, long[] e) {
int size = to - from;
int eIdx = 0;
int leftIdx = from; // 左边区间要处理的元素的下标
int rightIdx = mid; // 右边区间要处理的元素的下标
// 什么条件表示左边区间 [from, mid) 就没有元素了 leftIdx == mid
// 同理右边区间:rightIdx == to
// 两个区间都有元素的时候才要比较
// 左区间有元素:leftIdx < mid
// 右: rightIdx < to
// leftIdx < mid && rightIdx < to
while (leftIdx < mid && rightIdx < to) {
// 要比较的两个元素 array[leftIdx] 和 array[rightIdx]
if (array[leftIdx] <= array[rightIdx]) {
// 取左边的元素放入 e 数组
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
} else {
// 取右边的元素放入 e 数组
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// 说明有一个区间的元素被全部取完了
if (leftIdx < mid) {
// 右边取完了,把左边的所有元素依次放到 e 中
while (leftIdx < mid) {
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
}
} else {
while (rightIdx < to) {
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// e 里是有序的 [0, size)
// 要把有序的元素从 e 中搬回到 array 中 [from, to)
for (int i = 0; i < size; i++) {
array[from + i] = e[i];
}
}
3)
public static void mergeSort非递归版本(long[] array) {
long[] e = new long[array.length];
// i 个元素和 i 个元素要归并
for (int i = 1; i < array.length; i *= 2) { // Log(n)
// 内部的循环代表,代表一组一组完成归并
for (int j = 0; j < array.length; j += (2 * i)) { // O(n)
int from = j;
int mid = from + i;
if (mid >= array.length) {
// 说明没有右边的区间,本次不需要处理了
break;
}
int to = mid + i;
if (to > array.length) {
to = array.length;
}
merge2(array, from, mid, to, e);
}
}
}
private static void merge2(long[] array, int from, int mid, int to, long[] e) {
int size = to - from;
int eIdx = 0;
int leftIdx = from; // 左边区间要处理的元素的下标
int rightIdx = mid; // 右边区间要处理的元素的下标
// 什么条件表示左边区间 [from, mid) 就没有元素了 leftIdx == mid
// 同理右边区间:rightIdx == to// 两个区间都有元素的时候才要比较// 左区间有元素:leftIdx < mid
// 右: rightIdx < to// leftIdx < mid && rightIdx < to
while (leftIdx < mid && rightIdx < to) {
// 要比较的两个元素 array[leftIdx] 和 array[rightIdx]
if (array[leftIdx] <= array[rightIdx]) {
// 取左边的元素放入 e 数组
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
} else {// 取右边的元素放入 e 数组
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// 说明有一个区间的元素被全部取完了
if (leftIdx < mid) {
// 右边取完了,把左边的所有元素依次放到 e 中
while (leftIdx < mid) {
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
}
} else {
while (rightIdx < to) {
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// e 里是有序的 [0, size)
// 要把有序的元素从 e 中搬回到 array 中 [from, to)
for (int i = 0; i < size; i++) {
array[from + i] = e[i];
}
}