常用排序记录

前言

排序,就是是一串记录,按照其中的某个或某些关键字的大小,递增或者递减的排列起来的操作。

时间复杂度O(N²)

冒泡排序
思路

相邻的元素比较,如果左侧的数大于右边的数就交换,每一轮都会有一个数被确定位置

实现代码
public static void sort(int arr[]){
    if(arr == null || arr.length<2) return;
    for( int i = 0 ; i < arr.length - 1 ; i++ ){
        for(int j = 0;j < arr.length - 1 - i ; j++){
            int temp = 0;
            if(arr[j] < arr[j + 1]){
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
冒泡优化

冒泡有一个最大的问题就是这种算法不管不管你有序还是没序,闭着眼睛把你循环比较了再说。

比如我举个数组例子:[ 9,8,7,6,5 ],一个有序的数组,根本不需要排序,它仍然是双层循环一个不少的把数据遍历干净,这其实就是做了没必要做的事情,属于浪费资源。

针对这个问题,我们可以设定一个临时遍历来标记该数组是否已经有序,如果有序了就不用遍历了。

实现代码
public static void sort(int arr[]){
    for( int i = 0;i < arr.length - 1 ; i++ ){
        boolean isSort = true;
        for( int j = 0;j < arr.length - 1 - i ; j++ ){
            int temp = 0;
            if(arr[j] < arr[j + 1]){
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                isSort = false;
            }
        }
        if(isSort){
            break;
        }
    }
}


选择排序
思路

首先,找到数组中最小的元素,拎出来,将它和数组的第一个元素交换位置,第二步,在剩下的元素中继续寻找最小的元素,拎出来,和数组的第二个元素交换位置,如此循环,直到整个数组排序完成。

实现代码
public static void sort(int arr[]){
    for( int i = 0;i < arr.length ; i++ ){
        int min = i;//最小元素的下标
        for(int j = i + 1;j < arr.length ; j++ ){
            if(arr[j] < arr[min]){
                min = j;//找最小值
            }
        }
        //交换位置
        int temp = arr[i];
        arr[i] = arr[min];
        arr[min] = temp;
    }
}
插入排序
思路

插入排序的思想和我们打扑克摸牌的时候一样,从牌堆里一张一张摸起来的牌都是乱序的,我们会把摸起来的牌插入到左手中合适的位置,让左手中的牌时刻保持一个有序的状态。

那如果我们不是从牌堆里摸牌,而是左手里面初始化就是一堆乱牌呢? 一样的道理,我们把牌往手的右边挪一挪,把手的左边空出一点位置来,然后在乱牌中抽一张出来,插入到左边,再抽一张出来,插入到左边,再抽一张,插入到左边,每次插入都插入到左边合适的位置,时刻保持左边的牌是有序的,直到右边的牌抽完,则排序完毕。

代码实现

左程云老师的代码

public static void InsertionSort(int[] arr){
    if (arr == null || arr.length < 2) {
            return;
        }
    //从index为1的开始
    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){
    if(i!=j){
        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j]; 
    }

}
希尔排序
思路

我们知道,插入排序对于大规模的乱序数组的时候效率是比较慢的,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支

代码实现
public static void sort(int[] arr) {
    int length = arr.length;
    //区间
    int gap = 1;
    while (gap < length) {
        gap = gap * 3 + 1;
    }
    while (gap > 0) {
        for (int i = gap; i < length; i++) {
            int tmp = arr[i];
            int j = i - gap;
            //跨区间排序
            while (j >= 0 && arr[j] > tmp) {
                arr[j + gap] = arr[j];
                j -= gap;
            }
            arr[j + gap] = tmp;
        }
        gap = gap / 3;
    }
}

时间复杂度O(N×logN)

归并排序
思路

归并排序本身是个递归过程,左右两侧的数组直接递归进行排序。编写时只要关心如何将两个有序数组在时间复杂度为**O(N)**的情况下进行合并

先递归将数组分成两段排序,然后放到辅助数组中,整个数组排序

代码实现

在这里插入图片描述

//左程云老师的代码
public static void sortProcess(int[] arr,int L,int R){
    //base case
    //关键步骤。栈的操作,递归后变成两个有序的子数组
    if(L==R){
        return;
    }
    int mid = L + ((R-L)>>1);//也可以写成:L+((R-L)/2),这样写为了防止溢出,位运算比四则运算要快
    //两次递归得出两个有序的子数组
    sortProcess(arr,L,mid);
    sortProcess(arr,mid+1,R);
    merge(arr,L,mid,R);
}
//归并	
public static void merge(int[] arr,int L, int mid, int R){
    //辅助数组
    int[] help = new int[R-L+1];
    int i = 0;
    int p1 = L;
    int p2 = mid;
    while(p1<=mid && p2<=R){
        help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }
    //两个数组必有且只有一个越界
    while(p1<=mid){
        help[i++] = arr[p1++];
    }
    while(p2<=R){
        help[i++] = arr[p2++];
    }
    for(i=0; i<help.length;i++){
        arr[L+i] = help[i];
    }
}

用例1:求最小和问题

求一个数组中,每个数字左边比它小的数累加起来

例如一个数组[4,1,3,5,0,6],返回结果是22

代码实现

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){
    //base case
    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 i = 0;
    int p1 = L;
    int p2 = mid+1;
    int res = 0;
    while(p1<=mid && 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<=mid){
        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;
}

用例2:求一个数组中有多少个降序(逆序),方法同上

快速排序
思路

每次从序列中选出一个基准值,其他数依次和基准值作比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,。

单边扫描

快速排序的关键之处在于切分,切分的同时要进行比较和移动,这里介绍一种叫做单边扫描的做法。

我们随意抽取一个数作为基准值,同时设定一个标记 mark 代表左边序列最右侧的下标位置,当然初始为 0 ,接下来遍历数组,如果元素大于基准值,无操作,继续遍历,如果元素小于基准值,则把 mark + 1 ,再将 mark 所在位置的元素和遍历到的元素交换位置,mark 这个位置存储的是比基准值小的数据,当遍历结束后,将基准值与 mark 所在元素交换位置即可。

代码实现
public static void sort(int[] arr) {
    sort(arr, 0, arr.length - 1);
}

private static void sort(int[] arr, int startIndex, int endIndex) {
    if (endIndex <= startIndex) {
        return;
    }
    //切分
    int pivotIndex = partition(arr, startIndex, endIndex);
    sort(arr, startIndex, pivotIndex - 1);
    sort(arr, pivotIndex + 1, endIndex);
}

private static int partition(int[] arr, int startIndex, int endIndex) {
    int pivot = arr[startIndex];//取基准值
    int mark = startIndex;//Mark初始化为起始下标

    for(int i=startIndex+1; i<=endIndex; i++){
        if(arr[i]<pivot){
            //小于基准值 则mark+1,并交换位置。
            mark ++;
            int p = arr[mark];
            arr[mark] = arr[i];
            arr[i] = p;
        }
    }
    //基准值与mark对应元素调换位置
    arr[startIndex] = arr[mark];
    arr[mark] = pivot;
    return mark;
}
双边扫描

另外还有一种双边扫描的做法,看起来比较直观:我们随意抽取一个数作为基准值,然后从数组左右两边进行扫描,先从左往右找到一个大于基准值的元素,将下标指针记录下来,然后转到从右往左扫描,找到一个小于基准值的元素,交换这两个元素的位置,重复步骤,直到左右两个指针相遇,再将基准值与左侧最右边的元素交换。

不同之处只有 partition 方法:

代码实现
public static void sort(int[] arr) {
    sort(arr, 0, arr.length - 1);
}

private static void sort(int[] arr, int startIndex, int endIndex) {
    if (endIndex <= startIndex) {
        return;
    }
    //切分
    int pivotIndex = partition(arr, startIndex, endIndex);
    sort(arr, startIndex, pivotIndex-1);
    sort(arr, pivotIndex+1, endIndex);
}


public static int partition(int[] arr, int start, int end) {
		int left = start;
		int right = end;
		// 取第一个数做基准值
		int p = arr[start];

		while (true) {
            //顺序很重要,先从右开始
			// 从右往左扫描,找比基准值要小的
			while (arr[right] > p) {
				 right--;
				 if (left == right) {
						break;
						}
					}
			// 从左往右扫描,找比基准值要大的
			while (arr[left] <= p) {
				left++;
				if (left == right) {
					break;
				}
			}
			
			// 直到左右指针相遇
			if (left >= right) {
				break;
			}
			// 左右交换数据
			swap(arr, left, right);

		}
		// 将基准值插入序列
		int temp = arr[start];
		arr[start] = arr[right];
		arr[right] = temp;

		return right;
	}

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(logN)
  2. 建立堆(遍历+插入)O(N×log(N))
  3. 设计调整堆操作O(logN)
  4. 设计排序操作(遍历+调整堆)O(N×log(N))

用数组模拟堆结构,则下标为i的元素有以下特点

  • i == 0 时,无父节点
  • 父节点:(i - 1) / 2
  • 左节点: i * 2 + 1
  • 右节点: i * 2 + 2

在这里插入图片描述

代码实现
public static void sort(int[] arr) {
    int length = arr.length;
    //构建堆
    buildHeap(arr, length);
    for ( int i = length - 1; i > 0; i-- ) {
        //将堆顶元素与末位元素调换
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        //数组长度-1 隐藏堆尾元素
        length--;
        //将堆顶元素下沉 目的是将最大的元素浮到堆顶来
        sink(arr, 0, length);
    }
}
private static void buildHeap(int[] arr, int length) {
    for (int i = length / 2; i >= 0; i--) {
        sink(arr, i, length);
    }
}

/**
 * 下沉调整
 * @param arr 数组
 * @param index 调整位置
 * @param length 数组范围
 */
private static void sink(int[] arr, int index, int length) {
    int leftChild = 2 * index + 1;//左子节点下标
    int rightChild = 2 * index + 2;//右子节点下标
    int present = index;//要调整的节点下标

    //下沉左边
    if (leftChild < length && arr[leftChild] > arr[present]) {
        present = leftChild;
    }

    //下沉右边
    if (rightChild < length && arr[rightChild] > arr[present]) {
        present = rightChild;
    }

    //如果下标不相等 证明调换过了
    if (present != index) {
        //交换值
        int temp = arr[index];
        arr[index] = arr[present];
        arr[present] = temp;

        //继续下沉
        sink(arr, present, length);
    }
}

左程云老师的代码(思路一样)

public void sort(int[] arr){
    if(arr==null || arr.length<2){
        return;
    }
    heapSort(arr);
}
//堆排
public void heapSort(int[] arr){
    createHeap(arr);
    
    for(int i = arr.length - 1; i>0; i++){
        swap(arr,i,0);
        heapify(arr,0,i);
    }
}
//建立大根堆
public void createHeap(int[] arr){
    for(int i = 1;i<arr.length;i++){
        heapInsert(arr,i);
    }
}
//将一个位置插入堆中
public void heapInsert(int[] arr,int i){
    int parentIndex = (i-1)/2;
    while(arr[parentIndex]<arr[i]){
        //父节点小于子节点,才继续调整
        swap(arr,i,parentIndex);
        i = parentIndex;
        parentIndex = (i-1)/2;
    }
}
//从i开始调整堆结构
public void heapify(int[] arr,int i,int size){
    //左节点
    int L = i*2+1;
    //向下调整
    while(L<size){
        //左右较大的节点
        int bigger = L + 1 < size && arr[L+1] > arr[L] ? L + 1 : L;
        //左右根较大的
        bigger = arr[bigger] > arr[i] ? bigger : i;
        if(bigger == i){
            //调整完毕
            break;
        }
        //某个孩子比我大,继续调整
        swap(arr,bigger,i);
        i = bigger;
        L = i*2+1;
    }
}
public static void (int[] arr,int i,int j){
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}
桶排序

上述所讲都是基于比较的排序,而桶排序是基于数据状况的排序…有助于笔试帮助过case

桶排序包含两种排序:

  • 基数排序
  • 计数排序

在这里插入图片描述

注意

只使用于整数

实现代码
 public void sort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        // 最大值->桶的大小
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            max = Math.max(max, arr[i]);
        }
        int[] help = new int[max + 1];
        for (int i = 0; i < arr.length; i++) {
            // 对应的数+1
            help[arr[i]]++;
        }
        int index = 0;
        for (int i = 0; i < help.length; i++) {
            // 写回到原数组
            while(--help[i] > 0) {
                arr[index++] = i;
            }
        }
    }
各种排序特点总结

在这里插入图片描述

其中,最好,最坏,平均三项复杂度完全一样的就是与初始排序无关的排序方法,也就是:选择排序、堆排序、归并、基数

时间复杂度O(log2N)

二分查找
介绍

二分查找是一种查询效率非常高的查找算法。又称折半查找。

参考博客

思路

有序的序列,每次都是以序列的中间位置的数来与待查找的关键字进行比较,每次缩小一半的查找范围,直到匹配成功。

一个情景:将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

使用递归实现
/**
	 * 使用递归的二分查找
	 *@param arr 有序数组
	 *@param key 待查找关键字
	 *@return 找到的位置
	 */
	public static int recursionBinarySearch(int[] arr,int key,int low,int high){
		
		if(key < arr[low] || key > arr[high] || low > high){
			return -1;				
		}
		
		int middle = (low + high) / 2;			//初始中间位置
		if(arr[middle] > key){
			//比关键字大则关键字在左区域
			return recursionBinarySearch(arr, key, low, middle - 1);
		}else if(arr[middle] < key){
			//比关键字小则关键字在右区域
			return recursionBinarySearch(arr, key, middle + 1, high);
		}else {
			return middle;
		}	
		
	}
不使用递归实现
/**
	 * 不使用递归的二分查找
	 *@param arr
	 *@param key
	 *@return 关键字位置
	 */
	public static int commonBinarySearch(int[] arr,int key){
		int low = 0;
		int high = arr.length - 1;
		int middle = 0;			//定义middle
		
		if(key < arr[low] || key > arr[high] || low > high){
			return -1;				
		}
		
		while(low <= high){
			middle = (low + high) / 2;
			if(arr[middle] > key){
				//比关键字大则关键字在左区域
				high = middle - 1;
			}else if(arr[middle] < key){
				//比关键字小则关键字在右区域
				low = middle + 1;
			}else{
				return middle;
			}
		}
		
		return -1;		//最后仍然没有找到,则返回-1
	}
测试代码
public static void main(String[] args) {
 
		int[] arr = {1,3,5,7,9,11};
		int key = 4;
		//int position = recursionBinarySearch(arr,key,0,arr.length - 1);
		
		int position = commonBinarySearch(arr, key);
 
               if(position == -1){
			System.out.println("查找的是"+key+",序列中没有该数!");
		}else{
			System.out.println("查找的是"+key+",找到位置为:"+position);
		}
		
	}

recursionBinarySearch()的测试:key分别为0,9,10,15的查找结果


查找的是0,序列中没有该数!
 
查找的是9,找到位置为:4
 
查找的是10,序列中没有该数!
 
查找的是15,序列中没有该数!

commonBinarySearch()的测试:key分别为-1,5,6,20的查找结果

查找的是-1,序列中没有该数!
 
查找的是5,找到位置为:2
 
查找的是6,序列中没有该数!
 
查找的是20,序列中没有该数!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值