前言:仅用来自己复习,如果有读者发现错误,提出批评指正,非常感谢。
选择排序
选择排序:假设我们把数组分成有序区间和无序区间,有序区间开始时为0,无序区间为数组元素总个数,选择排序就是不断循环遍历无序区间。找到无序区间最值元素下标,再与无序区间第一个元素交换,使的有序区间越来越大,无序区间越来越小,最后整个区间有序。简而言之,选择排序就是遍历选择最值下标的过程。
public class SelectionSort {
public static void selectionSort(int[] arr){
if(arr == null || arr.length < 2){//首先判断数组是否为空,并且元素个数不能小于2,否则没有排序意义
return;
}
for(int i = 0; i < arr.length - 1;i++){//遍历数组,只需要遍历n-1次即可
int minIndex = i;//获取最小下标,先设为i
for(int j = i + 1;j < arr.length;j++){//从i+1开始遍历,直到最后
minIndex = arr[j] < arr[minIndex] ? j : minIndex;//判断当前位置元素与minIndex下标的值大小,并赋值给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;
}
public static void main(String[] args) {
int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};
selectionSort(arr3);
for (int n: arr4) {
System.out.print(n+" ");
}
}
}
冒泡排序
冒泡排序:假设我们将数组分为无序区间和有序区间,循环遍历无序区间,依次将相邻两个位置元素大小进行比较,满足排序条件则交换,直到将最值元素冒泡到无序区间最后,使得无序区间越来越小,有序区间越来越大,最后整个区间变得有序。与选择排序比较起来,冒泡排序在每一次遍历,两两比较并交换的过程,其实就是相当于在做排序,而选择排序是每次只交换最值元素。
public class BubbleSort {
public static void bubbleSort(int[] arr){
if(arr == null || arr.length < 2){//前提
return ;
}
for(int i = arr.length - 1; i > 0;i--){//循环遍历,n-1次
for(int j = 0; j < i;j++){
if(arr[j] > arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
private static void swap(int[] arr, int j, int i) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
public static void main(String[] args) {
int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};
bubbleSort(arr4);
for (int n: arr4) {
System.out.print(n+" ");
}
}
}
插入排序
插入排序适用于数据量较小的排序。假设将整个区间分为有序区间和无序区间,遍历整个数组,将无序区间的首元素插入到有序区间有合适位置,最后使得整个区间有序。
//插入排序
public class InsertionSort {
public static void insertSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i = 1;i < arr.length;i++){
for(int j = i;j > 0 && arr[j] < arr[j-1];j--){
swap(arr,j,j-1);
}
}
}
private 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];
}
public static void main(String[] args) {
int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};
insertSort(arr4);
for (int n: arr4) {
System.out.print(n+" ");
}
}
}
异或运算寻找奇数次元素
异或运算满足交换律和结合律,并且0^n = n,n^n = 0,这个知识是解决问题的关键
比较常见的两种题型:
第一种题型:找出数组中只有一个数字出现奇数次,其他都为偶数次的数。
解决方案,将0和所有数组元素进行异或运算,最后结果就是出现奇数次的数
第二种题型:找出数组中只有两个数组出现奇数次,其他都为偶数次的两个数
解决方案,先将0和所有数组元素进行异或运算,最后结果就为ab,又因为a!=b所以ab必将出现1位数字为1,此时将数组分为该位数字为1或0的两种数,再将0与其中一类所有数字异或就可以得出a或者b,最后再该结果与a^b进行异或,就可以得出另一个数了
//找数组中出现奇数次的数
public class OddTimesEvenTimes {
public static void printOddTimesNum1(int[] arr){//异或运算解决问题
int result = 0;
for(int n : arr){
result ^= n;
}
System.out.println(result);
}
public static void printOddTimesNum2(int[] arr){
int result1 = 0;
for(int n : arr){
result1 ^= n;
}
int rightOne = result1 & (~result1 + 1); //先将a^b取反,再加1,最后与a^b相与,得出最右位为1的结果
int result2 = 0;
for(int n : arr){
if((rightOne & n) != 0){//不能==1,因为1出现的位置并不一定是最低位,那么即使是都在相同为出现1了,相与结果也不一定为1
result2 ^= n;
}
}
System.out.println(result2+" "+(result2^result1));
}
public static void main(String[] args) {
int[] arr = new int[]{1,1,5,5,1,2,2,3,4,4,6,6,1};
int[] arr2 = new int[]{1,1,5,5,1,2,2,3,4,4,6,6};
printOddTimesNum1(arr);
printOddTimesNum2(arr2);
}
}
归并排序
归并排序在我认为更像是在做二分,整体思想就是将整个区间划分为两个,并分别对其进行排序,最后再合并的过程。
public class MergeSort {
//整体过程
public static void process(int[] arr,int l,int r){
if(l == r){//如果l==r了,说明当前区间只有一个数,也就不需要进行排序了。
return;
}
int m = (l+r)/2;
process(arr,l,m);
process(arr,m+1,r);
merge(arr,l,m,r);
}
//合并过程
private static void merge(int[] arr, int l, int m, int r) {
//开辟r-l+1个元素大小的空间,用来合并两个有序区间
int[] temps = new int[r-l+1];
//用两个指针分别去遍历这两个有序区间
int lCur = l;
int rCur = m+1;
int i = 0;
while(lCur <= m && rCur <= r){
temps[i++] = arr[lCur] < arr[rCur] ? arr[lCur++] : arr[rCur++];
}
while(lCur <= m){
temps[i++] = arr[lCur++];
}
while(rCur <= r){
temps[i++] = arr[rCur++];
}
//再将排好序的数组重新放入原数组中
for(int j = 0;j < temps.length;j++){
arr[l+j] = temps[j];
}
}
public static void main(String[] args) {
int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};
process(arr4,0,arr4.length-1);
for (int n: arr4) {
System.out.print(n+" ");
}
}
}
归并排序拓展
小和问题
在一个数组中,每个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例如:[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 class SumNum {
public static int sumNum(int[] arr){
if(arr == null || arr.length < 2){
return 0;
}
return process(arr,0,arr.length-1);
}
private static int process(int[] arr, int l, int r) {
if(l == r)return 0;
int m = (l+r)/2;
return process(arr,l,m)
+process(arr,m+1,r)
+merge(arr,l,m,r);
}
private static int merge(int[] arr, int l, int m, int r) {
int[] temps = new int[r-l+1];
int re = 0;
int p1 = l;
int p2 = m+1;
int i = 0;
while(p1 <= m && p2 <= r){
re += arr[p1] < arr[p2] ? (r-p2+1)*arr[p1] : 0;
temps[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= m){
temps[i++] = arr[p1++];
}
while(p2 <= r){
temps[i++] = arr[p2++];
}
for(i = 0;i < temps.length;i++){
arr[l+i] = temps[i];
}
return re;
}
public static void main(String[] args) {
int[] arr = new int[]{1,3,4,2,5};
System.out.println(sumNum(arr));
}
}
逆序对问题
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序对。
解决方案:有了上面小和问题的理解,那么这道题就很简单了,同理!
public class ReversedOrder {
public static void reversedOrder(int[] arr){
if(arr == null || arr.length < 2){
return;
}
process(arr,0,arr.length-1);
}
private static void process(int[] arr, int l, int r) {
if(l == r){
return;
}
int m = (l+r)/2;
process(arr,l,m);
process(arr,m+1,r);
merge(arr,l,m,r);
}
private static void merge(int[] arr, int l, int m, int r) {
int[] temps = new int[r-l+1];
int i = 0;
int p1 = l;
int p2 = m+1;
while(p1 <= m && p2 <= r){
if(arr[p1] > arr[p2]){
int j = p2;
while(j <= r) {
System.out.println(arr[p1]+","+arr[j++]);
}
temps[i++] = arr[p1++];
}else{
temps[i++] = arr[p2++];
}
}
while(p1 <= m){
temps[i++] = arr[p1++];
}
while(p2 <= r){
temps[i++] = arr[p2++];
}
for(i = 0;i < temps.length;i++){
arr[l+i] = temps[i];
}
}
public static void main(String[] args) {
int[] arr = new int[]{3,2,4,5,0};
reversedOrder(arr);
}
}
快速排序
快速排序是寻找一个基准值,把小的放在左边,大的放在右边,依次二分,使得整个区间变得有序。它的时间复杂度的关键就在于partition的过程,也就是寻找基准值并进行排大小的过程,下面这段代码还是有些粗糙了,可以再详细去查一下快排,会有很多种partition的变形,从而达到提高效率的目的。
public class QuickSort {
public static void quickSort(int[] arr,int l,int r){
if(l < r){
swap(arr,l+(int)(Math.random()*(r-l+1)),r);//随机抽取区间内任意一个元素作为基准值,并放在区间末尾
int[] p = partition(arr,l,r);//整个区间进行partition,使得整个区间分为小于、等于、大于三个区间
quickSort(arr,l,p[0]-1);//对小于区间继续进行快排
quickSort(arr,p[1]+1,r);//对大于区间继续进行快排
}
}
private static int[] partition(int[] arr, int l, int r) {
//创建两个指针,分别代表小于区间右指针和大于区间做指针
int less = l-1;//默认是该区间最左边的前一个位置
int more = r;//默认是该区间最右边的位置,因为最右边是基准值,不算做需要去比较的区间内
while(l < more){//l指针才是真正移动的指针,需要去判断的区域也是该指针到more的这个区间
if(arr[l] < arr[r]){//判断是小于的元素,则需要将其与小于区间右侧元素交换
swap(arr,++less,l++);
}else if(arr[l] > arr[r]){
swap(arr,l,--more);
}else{
l++;
}
}
swap(arr,more,r);
return new int[]{less,more};
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};
quickSort(arr3,0,arr3.length-1);
for (int n: arr3) {
System.out.print(n+" ");
}
}
}
堆排序
大根堆和小根堆概念:即根结点元素是堆里所有结点的最大者或者最小者。
堆的数据结构实际上就是一颗完全二叉树,并且其中任意一个结点的值总是不大于或不小于其父节点的值,所以要么就是大根堆,要么就是小根堆。
已知某个结点位置是index,那么它的父节点位置就是(index-1)/2,左孩子结点位置就是index*2+1,右孩子结点位置就是index * 2+2。
堆排序最核心的两个步骤就是向上调整和向下调整。下面是以大根堆为例。
向上调整即已知某个节点位置,比较该节点元素是否大于父节点元素,如果大则交换,并继续向上比较,否则退出。
向下调整即已知某个节点位置,比较左右孩子的大小,将大的孩子与该节点进行比较,如果孩子大于父亲则交换,并继续向下调整,否则退出。
堆排序则是将大根堆的首元素(最大元素)与最后一个元素进行交换,并将其踢出当前堆(heapSize–),再对首元素进行向下调整,凑成新的大根堆,循环该操作,直到heapSize为0,即整个区间有序。
public class HeapSort {
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
//现需要建立大根堆
//循环遍历数组,让元素依次插入到堆中
for(int i = 0;i < arr.length;i++){
heapInsert(arr,i);
}
//循环将堆顶元素放到最后,并缩小堆大小
int heapSize = arr.length;
//先将现在第一个元素与最后一个元素进行交换
swap(arr,0,--heapSize);
while(heapSize > 0){
//将堆顶元素向下调整
heapify(arr,0,heapSize);
swap(arr,0,--heapSize);
}
}
//向上调整
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;
}
}
//向下调整
public static void heapify(int[] arr,int index,int heapSize){
int left = index*2+1;
while(left < heapSize){
//找到左右孩子的最大值
int lagest = left+1 < heapSize && arr[left] < arr[left+1] ? left+1 : left;
//最大值和父节点进行比较
lagest = arr[lagest] > arr[index] ? lagest : index;
if(lagest == index){
break;
}
swap(arr,lagest,index);
index = lagest;
left = index*2+1;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};
heapSort(arr4);
for (int n: arr4) {
System.out.print(n+" ");
}
}
}
根据代码,我们可以看出建堆的过程时间复杂度的O(N* logN)的,其实还有更快的方式,那就是从后往前进行heapify操作,为什么呢?因为任何一棵树的左右子树满足堆的要求,那么只需要将根结点向下调整,那么该树就一定满足堆结构,根据计算得出,这种方式的时间复杂度是O(N)的。
当然,也不难发现,叶子结点其实是无序进行向下调整的,所以可以直接从最后一个父节点开始。
//建堆的第二种方式
//寻找最后一个父节点,即heapSize-1(最后一个节点)的父节点
int index = (arr.length-2)/2;
for(;index >= 0;index--){
heapify(arr,index,arr.length);
}