前言
排序,就是是一串记录,按照其中的某个或某些关键字的大小,递增或者递减的排列起来的操作。
时间复杂度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];
}
堆排序
思路
我们完全可以把堆(以下全都默认为最大堆)看成一棵完全二叉树,但是位于堆顶的元素总是整棵树的最大值,每个子节点的值都比父节点小,由于堆要时刻保持这样的规则特性,所以一旦堆里面的数据发生变化,我们必须对堆重新进行一次构建。
既然堆顶元素永远都是整棵树中的最大值,那么我们将数据构建成堆后,只需要从堆顶取元素不就好了吗? 第一次取的元素,是否取的就是最大值?取完后把堆重新构建一下,然后再取堆顶的元素,是否取的就是第二大的值? 反复的取,取出来的数据也就是有序的数据。
- 设计一个元素插入堆操作O(logN)
- 建立堆(遍历+插入)O(N×log(N))
- 设计调整堆操作O(logN)
- 设计排序操作(遍历+调整堆)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,序列中没有该数!