排序算法及相关问题


一、选择排序和冒泡排序

时间复杂度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)

计算复杂度:

  1. log(b,a) > d -> 复杂度为O(N^log(b,a))
  2. log(b,a) = d -> 复杂度为O(N^d * logN)
  3. 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(N
logN)
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),又稳定的排序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值