排序算法及相关问题
一、选择排序和冒泡排序
时间复杂度O(N^2),额外空间复杂度O(1)。
1、选择排序
- 遍历数组,找出最小的值放到前面。重复操作。
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i; //记录最小值
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex); //交换i和找到的minIndex位置的值
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
2、冒泡排序
- 遍历数组,比较后一个值,大的值放到后面,遍历完一遍最大的在最后。重复操作。
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
//异或运算,交换数组元素
//异或运算可以理解为无进位相加(1^1=1+1=0,1^0=1+0=1)
//0^N=N, N^N=0
//a^b=b^a, (a^b)^c=a^(b^c)
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
/*
前提,i和j不能是内存的同一位置,否则是跟自己在异或,为0
*/
}
二、插入排序
时间复杂度:最差情况O(N^2)(比如:7,6,5,4,3,2,1),最好情况O(N)(比如:1,2,3,4,5,6,7)。时间复杂度按照最差情况来估计。
额外空间复杂度O(1)。
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
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);
}
}
}
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
三、二分法
1、在一个有序数组中,找某个数是否存在
- 可以遍历查找,复杂度O(N)。二分法查找最快,时间复杂度O(logN)。
public static boolean exist(int[] sortedArr, int num) {
if (sortedArr == null || sortedArr.length == 0) {
return false;
}
int L = 0;
int R = sortedArr.length - 1;
int mid = 0;
while (L < R) {
mid = L + ((R - L) >> 1); //求中点, (R-L)>>1 右移一位,相当于除以2
if (sortedArr[mid] == num) {
return true;
} else if (sortedArr[mid] > num) {
R = mid - 1;
} else {
L = mid + 1;
}
}
return sortedArr[L] == num;
}
2、在一个有序数组中,找>=某个数最左侧的位置
// 在arr上,找满足>=value的最左位置
public static int nearestIndex(int[] arr, int value) {
int L = 0;
int R = arr.length - 1;
int index = -1;
while (L < R) {
int mid = L + ((R - L) >> 1);
if (arr[mid] >= value) {
index = mid;
R = mid - 1;
} else {
L = mid + 1;
}
}
return index;
}
3、局部最小值问题
- 无序数组,任何两个相邻的数不相等
- 求一个局部最小的数(局部最小:比相邻两个数都小)
- 时间复杂度小于O(logN)
public static int getLessIndex(int[] arr) {
if (arr == null || arr.length == 0) {
return -1; // no exist
}
// 数组的开头,如果arr[0] < arr[1] ,arr[0]被定义为局部最小
if (arr.length == 1 || arr[0] < arr[1]) {
return 0;
}
//数组的结尾,如果arr[N-1] < arr[N-2] ,arr[N-1]被定义为局部最小
if (arr[arr.length - 1] < arr[arr.length - 2]) {
return arr.length - 1;
}
int left = 1;
int right = arr.length - 2;
int mid = 0;
while (left < right) {
mid = (left + right) / 2;
if (arr[mid] > arr[mid - 1]) {
right = mid - 1;
} else if (arr[mid] > arr[mid + 1]) {
left = mid + 1;
} else {
return mid;
}
}
return left;
}
四、归并排序
- 整体就是一个简单递归,左边排好序、右边排好序、让其整体有序
- 让其整体有序的过程里用了外排序方法
- 利用master公式来求解时间复杂度
- 归并排序的实质
- 时间复杂度O(N*logN),额外空间复杂度O(N)
master公式
T(N) = a*T(N/b) + O(N^d)
计算复杂度:
- log(b,a) > d -> 复杂度为O(N^log(b,a))
- log(b,a) = d -> 复杂度为O(N^d * logN)
- log(b,a) < d ->
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1); //二分
mergeSort(arr, l, mid); //左排
mergeSort(arr, mid + 1, r); //右排
merge(arr, l, mid, r); //合起来排
}
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1]; //创建一个和原数组一样大小的临时数组
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++]; //P2越界,触发这个while,把左半部分剩下的放到临时
}
while (p2 <= r) {
help[i++] = arr[p2++]; //P1越界,触发这个while,把右半部分剩下的放到临时
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
选择排序、冒泡排序、插入排序时间复杂度都是O(N^2),因为浪费了大量的比较行为
归并排序的扩展
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组
的小和。求一个数组 的小和。
例子:[1,3,4,2,5]
1:左边比1小的数没有;
3:左边比3小的数1;
4:左边比4小的数1、3;
2:左边比2小的数1;
5:左边比5小的数1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16
- 思路:看成每个数与右边的数比较,如果右边更大,有几个数比自己大则累加自己几次。可以通过归并排序过程中进行判断。
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return mergeSort(arr, l, mid)
+ mergeSort(arr, mid + 1, r)
+ merge(arr, l, mid, r);
}
public static int merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}
逆序对问题
在一个数组中,左边的数如果比右边的数大,则折两个数
构成一个逆序对,请打印所有逆序对
public static int mergeSort(int[] arr){
if(arr == null || arr.length < 2){
return 0;
}
return mergeSort(arr,0,arr.length-1);
}
public static int mergeSort(int[] arr,int l,int r){
if(l == r){
return 0;
}
int mid = l + ((r - l)>>1);
return mergeSort(arr,l,mid) + mergeSort(arr,mid + 1,r) + merge(arr,l,mid,r);
}
public static int merge(int[] arr,int l,int mid,int r){
int[] help = new int[r-l+1];
int count = 0;
int p1 = l;
int p2 = mid + 1;
int i = 0;
while(p1<=mid && p2<=r){
//此处,在合并时,比较两组的数据,由于都是升序排列,因此逆序对需要如下这样计算。
count += arr[p1] > arr[p2] ? (mid - p1 + 1) : 0;
help[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=mid){
help[i++] = arr[p1++];
}
while(p2<=r){
help[i++] = arr[p2++];
}
//最后将help数组中的元素拷贝到原数组中。
for(i=0; i<help.length;i++){
arr[l+i] = help[i];
}
}
五、快速排序
1、荷兰国旗问题
给定一个数组arr,和一个数num,请把小于num的数放在数组的 左边,等于num的数放
在数组的中间,大于num的数放在数组的 右边。要求额外空间复杂度O(1),时间复杂度
O(N)
public static int[] partition(int[] arr, int l, int r, int p) {
int less = l - 1;
int more = r + 1;
while (l < more) {
if (arr[l] < p) {
swap(arr, ++less, l++);
} else if (arr[l] > p) {
swap(arr, --more, l);
} else {
l++;
}
}
return new int[] { less + 1, more - 1 };
}
2、基于荷兰国旗问题来进行快速排序
- 随机选择数组中的一个数最为中间数做荷兰国旗,小于他的数放左边,大于他的数放右边。对左右两边重复递归。
- 随机选择的数有好情况,有坏情况,都是等概率事件。时间复杂度O(N*logN)。
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
swap(arr, l + (int) (Math.random() * (r - l + 1)), r); //l + (int) (Math.random() * (r - l + 1)) 随机选一个数
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1, r);
}
}
//分成三部分 <p ==p >p
//返回 等于区域(左边界,右边界)
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
六、堆排序
1、堆结构
堆结构就是用数组实现的完全二叉树结构。可以将一段从0开始的连续数组看成完全二叉树结构。
i位置:左孩子下标2i+1,右孩子下标2i+2。父节点(i-1)/2。
- 堆是完全二叉树结构
- 大根堆:每一棵子树的最大值就是头节点的值
- 小根堆:每一棵子树的最小值就是头节点的值
2、堆排序
1、先让整个数组都变成大根堆结构,建立堆的过程:1) 从上到下的方法,时间复杂度为O(NlogN);2) 从下到上的方法,时间复杂度为O(N)
2、把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调
整堆,一直周而复始,时间复杂度为O(NlogN)
3、堆的大小减小成0之后,排序完成
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// for (int i = 0; i < arr.length; i++) { // O(N)
// heapInsert(arr, i); // 变成大根堆,O(logN)
// }
for (int i = arr.length - 1; i >= 0; i--) {
heapify(arr, i, arr.length); // 变成大根堆
}
int size = arr.length;
swap(arr, 0, --size); //把0位置的数(数组最大值)和最后一个数交换,堆大小--
while (size > 0) { // O(N)
heapify(arr, 0, size); // O(logN)
swap(arr, 0, --size); // O(1)
}
}
//某个数现在在index位置,往上继续移动
public static void heapInsert(int[] arr, int index) {
//孩子的值大于父值,交换
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) /2);
index = (index - 1)/2 ;
}
}
//某个数在index位置,能否往下移动
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1; //左孩子的下标
while (left < size) { //下方还有孩子的时候
//两个孩子谁值大,把下标给largest
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
//父和孩子谁的值大,把下标给largest
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
3、堆排序扩展题目
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元
素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的
排序算法针对这个数据进行排序。
public void sortedArrDistanceLessK(int[] arr, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(); //优先级队列,堆结构,小根堆
int index = 0;
for (; index < Math.min(arr.length, k); index++) {
heap.add(arr[index]);
}
int i = 0;
for (; index < arr.length; i++, index++) {
heap.add(arr[index]);
arr[i] = heap.poll();
}
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
}
七、基数排序
桶排序思想下的排序
1)计数排序
2)基数排序
分析:
1)桶排序思想下的排序都是不基于比较的排序
2)时间复杂度为O(N),额外空间负载度O(M)
3)应用范围有限,需要样本的数据状况满足桶的划分
//取出该位置上的数,比如 x =789,d =1 取出个位数字9 d= 2取出十位数字8
public static int getDigit(int x ,int d){
x = Math.abs(x);
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
//获得一个数组中最大值的位数 比如数组中的最大值为499,那么就返回3
public static int maxbits(int[] arrs){
int max = Integer.MIN_VALUE;//获取系统中的最小值,防止越界,可以看做保险措施
for (int i = 0; i < arrs.length; i++) {
max = Math.max(max, arrs[i]);
}
int res = 0;
while (max != 0){
res++;
max /=10;
}
return res;
}
//适用范围更广,radixSort(arr, 0, arr.length - 1, maxbits(arr)); digit是位数,就看做个十百
public static int[] radixsort(int[] arrs, int begin, int end, int digit) {
final int radix = 10;//写死了,就是10进制
int i = 0;
int j = 0;
int[] help = new int[end - begin + 1];//创建一个帮助数组,大小和原数组一样
for (int d = 1; d <= digit; d++) {
//这个大循环很关键,依照之前的分析,进出桶的次数和该数组的最大值的位数一样,比如数组中的最大值为499,那么就进出桶3次(最小也为1)
int[] count = new int[radix];//咱们都写得是十进制的排序,那么桶无非就是0.1.2...9,这里去看TODO里所述的优化,
// 统计位上出现频数 比如针对d=1,那就是个位,个位数取出来为j,count数组上相应位置+1
for (i = begin; i <= end ; i++) {
j = getDigit(arrs[i], d);
count[j]++;
}
for (int k = 1; k <radix ; k++) {
count[k] = count[k]+count[k-1];//累加 达到一种效果 ,比如个位数<=3的个数有7个
//此时 count[0] 表示 数组中当前位(d位)是0的数字有多少个
//count[1] 表示 数组中当前位(d位)是0和1的数字有多少个 依次类推 直到count[9]
}
for (i =end;i>=begin;i--){//从右往左
j = getDigit(arrs[i], d); //再把相应位上的数取出来,无非就是0-9;
//!!!以下两句非常关键简洁,出桶,利用help数组。把原始数放到频数-1的位置就是出桶,每放完一次,频数--
help[count[j] - 1] = arrs[i];
count[j]--;
}
//help数组完成使命,对arrs再做一次规整
for(i = begin,j = 0;i<=end;i++,j++){ //注意啊,这里的j只是临时变量,用作循环数组help
arrs[i] = help[j];
}
}
return arrs;
}
总结
排序算法的稳定性及其汇总
同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定
性的;否则就没有。
- 不具备稳定性的排序:
选择排序、快速排序、堆排序 - 具备稳定性的排序:
冒泡排序、插入排序、归并排序、一切桶排序思想下的排序 - 目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。