排序算法
-
复杂度
- 时间复杂度:执行时间与数据规模的关系
- 空间复杂度:存储空间与数据规模的关系
-
稳定性
- 稳定:一组数据中,a在b的前面并且a==b,排序后a依旧在b的前面
- 不稳定:一组数据中,a在b的前面并且a==b,排序后a可能在b的前面
-
原地算法
- 原地算法不依赖额外的资源,或者依赖少数的额外资源,仅依靠输出来覆盖输入
- 空间复杂度为O(1)的都可以认为是原地算法
-
一般面试考察点:
- 分析某种具体的排序算法 时间 |空间| 稳定性
- 对比某几种排序算法
- 代码实现某几种排序算法 O(nlogn) 重点: 快速排序,归并排序,堆排序
一:冒泡排序[Bubble sort]
- 比较相邻元素
- 大的元素后置
- 嵌套循环,每次查看相邻的元素,如果逆序,则交换
- O(n^2)
/**
* 冒泡排序
*/
private static void bubbleSort(int[] nums) {
if (nums.length == 0){return;}
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums.length-i-1; j++) {
if(nums[j] > nums[j+1]){
//相邻元素比较,判断是不要交换
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
-
优化:如果序列已经完全有序,可以提前终止冒泡排序
private static void bubbleSort(int[] nums) { if (nums.length == 0){return;} boolean sorted = true; //1:假定有序 for (int i = 0; i < nums.length; i++) { for (int j = 0; j < nums.length-i-1; j++) { if(nums[j] > nums[j+1]){ //相邻元素比较,判断是不要交换 int temp = nums[j]; nums[j] = nums[j+1]; nums[j+1] = temp; sorted = false; //2进来了说明无序 } } //3 有序就直接break--看情况走不走这一步 if (sorted) {break;} } }
二:选择排序[SelectionSort]
- 找到最小的元素
- 放到已经排序序列的末尾
- O(n^2)
private static void selectionSort(int[] nums) {
//nums[i...n排序]
for (int i = 0; i < nums.length; i++) {
int minIndex = i;
for (int j = i; j < nums.length; j++) { //从i出发
if(nums[j] < nums[minIndex]){
//找到nums[i...n]中的最小值的索引
minIndex = j;
}
}
//找到最小的索引之后交换==》 nums[i...n]中的最小值要放到nums[i]的位置上
int temp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = temp;
}
}
三:插入排序(InsertionSrot)
- 遍历数组
- 插入到已经排序的序列中的正确位置 (需要比较:比前面的数小,就交换)
- 从前到到逐步构建有序序列;对于未排序数组,在已排序序列中从后向前扫描,找到相应的位置,并插入
- O(n^2)
private static void insertionSort(int[] nums) {
if(nums.length==0){return;}
for (int i = 0; i < nums.length; i++) {
//将nums[i]插入到合适的位置
for (int j = i; j-1 >=0 ; j--) { //j从i开始,循环比较它前面的一个元素
if(nums[j] < nums[j-1]){
//交换
int temp = nums[j-1];
nums[j-1] = nums[j];
nums[j] = temp;
}else{
break;
}
}
}
}
注意边界:int j = i; j-1 >=0 ; j--
四:快速排序(QuickSort)
-
挑一个基准元素(pivot)
-
小于pivot放前面,pivot,大于pivot放后面 [<pivot] pivot [>pivot]
-
然后依次对左边和右边的子数组进行快排,最后达到有序
-
O(NlogN)
public class QuickSort {
private static void quickSort(int[] nums,int l,int r) {
if (l < r){
int p = partition(nums,l,r);
quickSort(nums,l,p-1); //递归
quickSort(nums,p+1,r); //递归
}
}
/**
*
* @param nums 区间数组
* @param l 区间数组最左边的下标
* @param r 区间数组最右边的下标
* @return
*/
private static int partition(int[] nums,int l ,int r){
//nums[l+1...j] < v; v; nums[j+1...i]>=v
int j = l;
for (int i = l+1; i <=r ; i++) {
if(nums[i] < nums[l]){
j++;
//交换
swap(nums,i,j);
} //大于和等于的话,i直接++
}
swap(nums,l,j); //最后一步交换nums[l]和nums[j]的值,将pivot的值放到中间
return j; //返回j的下标
}
//交换元素
private static void swap(int[] nums,int a,int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
public static void main(String[] args) {
int [] arrs = {5,3,1,7,2,6};
quickSort(arrs,0,arrs.length-1);
for (int arr:arrs){
System.out.println(arr);
}
}
}
public class QuickSort2 {
public static void quickSort(int[] array, int begin, int end) {
if (end <= begin) return;
int pivot = partition(array, begin, end);
quickSort(array, begin, pivot - 1);
quickSort(array, pivot + 1, end);
}
static int partition(int[] a, int begin, int end) {
// pivot: 标杆位置取最右边,counter: 小于下标pivot的元素的个数,最开始是0个,然后1个,2个
int pivot = end, counter = begin;
for (int i = begin; i < end; i++) {
if (a[i] < a[pivot]) {
int temp = a[counter];
a[counter] = a[i];
a[i] = temp;
counter++;
// counter的下标如果是3,表示它前面有3个元素小于pivot下标的值,它自己的值一定是大于pivot的值的,它也就是最后的大于pivot下标值的位置
}
}
//交换pivot和counter的值,使其达到 [<pivot] pivot [>pivot]
int temp = a[pivot];
a[pivot] = a[counter];
a[counter] = temp;
return counter;
}
public static void main(String[] args) {
int [] arrs = {5,3,1,7,2,6,4};
quickSort(arrs,0,arrs.length-1);
for (int arr:arrs){
System.out.println(arr);
}
}
}
五:归并排序[MergeSort]
- 将一组数据分成2半排序
- 合并两半
public class MergeSort {
public static void mergeSort(int[] nums ,int l ,int r){
if(l>=r){return;}
int mid = l + (( r - l) >>1);
mergeSort(nums,l,mid);
mergeSort(nums,mid+1,r);
merge(nums,l,mid,r);
}
//合并两个有序的区间 arr[l,mid]和arr[mid+1,r]
private static void merge(int[] nums,int l,int mid,int r){
//复制数组
int[] temp = Arrays.copyOfRange(nums,l,r+1); //最右边的是不包含的,所以要+1
int i =l,j=mid+1;
//每轮循环为arr[k]赋值,从之前复制的数组中拷贝到nums中
for (int k = l; k <= r ; k++) {
//处理越界
if(i > mid){ //左边区间不再有元素了,就把右边的值直接拷贝到arr中
nums[k] = temp[j-l];j++;
}else if( j > r){ //右边区间不再有元素了,直接拷贝左边的值到arr
nums[k] = temp[i-l];i++;
}
//比较arr[i]和arr[j]
else if(temp[i-l] <= temp[j-l] ){
nums[k] = temp[i-l];i++;
}else{
nums[k]=temp[j-l];j++;
}
}
}
public static void main(String[] args) {
int [] arrs = {5,3,1,7,2,6,9,8};
mergeSort(arrs,0,arrs.length-1);
for (int arr:arrs){
System.out.println(arr);
}
}
}
public class MergeSort2 {
public static void mergeSort(int[] array,int left,int right){
if(right <= left){return;}
int mid = left + (right- left)/2;
mergeSort(array,left,mid);
mergeSort(array,mid+1,right);
merge(array,left,mid,right);
}
private static void merge(int[] array, int left, int mid, int right) {
//1:开辟一个辅助数组
int[] temp = new int[right-left+1];
//2:合并有序数组
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right){
temp[k++] = array[i] <= array[j] ? array[i++]:array[j++];
}
while (i <= mid ){ temp[k++]=array[i++]; }
while (j <=right ){ temp[k++]=array[j++];}
//3:拷贝数组
for (int p = 0; p < temp.length ; p++) {
array[left+p] = temp[p];
}
}
public static void main(String[] args) {
int [] arrs = {5,3,1,7,2,6,9,8};
mergeSort(arrs,0,arrs.length-1);
for (int arr:arrs){
System.out.println(arr);
}
}
}
归并和快排:
- 快排:先调配出左右子数组,然后对于左右子数组进行排序
- 归并:先排序左右子数组,然后合并有序子数组
六:堆排序(HeapSort)
- 使用堆结构
- 构造一个大顶堆
- 取堆顶元素,交换到数组末尾,再将剩下的数字构建一个大顶堆
- 重复以上操作,直到取完堆中的数字,最终得到一个从小到大排列的序列
- 时间复杂度:建堆的时间复杂度为O(N),heapify的时间复杂度为O(logN),
- 堆排序对N个数进行heapify,所以时间复杂度为 O(NlogN)
/**
* 堆排序
* 1:构造一个大顶堆
* 2: 取堆顶元素,交换到数组末尾,再将剩下的数字构建一个大顶堆
* 3: 重复以上操作,直到取完堆中的数字,最终得到一个从小到大排列的序列
*/
public class HeapSort {
/**
* 维护堆的性质
* @param array 存储堆的数组
* @param length 数组长度
* @param i 待维护结点的下标
*/
static void heapify(int[] array, int length, int i) {
/**
* 找到父节点和左右孩子中最大的值的下标
*/
int left = 2 * i + 1, right = 2 * i + 2;
int largest = i;
if (left < length && array[largest] < array[left]) {
largest = left;
}
if (right < length && array[largest] < array[right]) {
largest = right;
}
/**
* 判断是不是左右孩子中 有结点的值 比 原父节点大
* 交换
*/
if (largest != i) {
int temp = array[i];
array[i] = array[largest];
array[largest] = temp;
//交换完之后,还要递归维护 交换后,孩子的堆的性质
heapify(array, length, largest);
}
}
public static void heapSort(int[] array) {
if (array.length == 0) {
return;
}
int length = array.length;
/**
* 建立堆:自下而上的下滤操作
* length / 2 - 1 ===》最后一个非叶子节点--》第一个叶子节点的size-1
*/
for (int i = length / 2 - 1; i >= 0; i--) {
heapify(array, length, i);
}
/**
* 排序: 取堆顶的数字(array[0]) 和 堆末尾的数字(array[length-1]) 交换,最大值堆顶元素就到数组末尾了
* 交换之后维护(剩余元素的)的堆的性质
*/
for (int i = length - 1; i >= 0; i--) {
int temp = array[0];
array[0] = array[i];
array[i] = temp;
heapify(array, i, 0);
}
}
public static void main(String[] args) {
int [] arrs = {5,3,1,7,2,6};
heapSort(arrs);
for (int arr:arrs){
System.out.println(arr);
}
}
}
七; 希尔排序(shell sort)
- 一种高效的插入排序:让数组越来越有序
- 希尔增量 gap =length/2 length/4 length/8 .... 1
- 每组进行插入排序
public class ShellSort {
/**
* 希尔排序
* @param nums
*/
private static void shellSort(int[] nums) {
int h = nums.length >> 1;
while( h >= 1){
for (int start = 0; start < h; start++) { //h循环
//对nums[start,start+h,start+2h....],进行插入排序--下面两个for循环就是插入排序
for (int i = start + h; i < nums.length; i+=h) {
int t = nums[i]; // 当前要插入的元素
int j;
for (j = i; j-h >=0 && t <= nums[j-h] ; j-=h) {
nums[j] = nums[j-h]; //将元素右移
}
nums[j] = t;
}
}
h = h >> 1; //gap
}
}
public static void main(String[] args) {
int [] arrs = {5,3,1,7,2,6};
shellSort(arrs);
for (int arr:arrs){
System.out.println(arr);
}
}
}
八:计数排序(Counting Sort)
-
计数排序要求输入的数据必须是有确定范围的整数。将输入的数据值转化为键存
储在额外开辟的数组空间中;然后依次把计数大于 1 的填充回原数组 -
步骤:
- 1:找最大值
- 2:遍历,下标就是这个数,数组[下标]的值就是它的个数
- 3:按照顺序遍历赋值
public class CountingSort { /** * 计数排序 * @param nums */ private static void countingSort(int[] nums) { if (nums.length == 0){return;} //1:找最大值 int max = nums[0]; for (int i = 1; i < nums.length; i++) { if (nums[i] > max){ max = nums[i]; } } //2:遍历数组统计 , 创建一个辅助数组 int[] counts = new int[max+1]; for (int i = 0; i < nums.length; i++) { counts[nums[i]]++; } //3:按照顺序赋值 int index = 0; for (int i = 0; i < counts.length; i++) { while (counts[i]-- > 0){ nums[index++] = i; } } } public static void main(String[] args) { int [] arrs = {5,5,5,3,1,7,2,6}; countingSort(arrs); for (int arr:arrs){ System.out.println(arr); } } }
九:桶排序(Bucket Sort)
- 待排序的序列分到若干个桶中,每个桶内的元素再进行个别排序。---不同数类型数组,桶排序的实现方式可能不同
- 时间复杂度最好可能是线性O(n),桶排序不是基于比较的排序
代码略
10:基数排序(Radix Sort)
-
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类
推,直到最高位。 -
非负数整数
-
对数的位置进行排序,先排序个位数,再排序十位数,百位数
public class RadixSort {
/**
* 基数排序---LSD实现 + 桶数组
* @param array
*/
private static void radixSort(int[] array) {
// 找出最大值, 方便知道它的位数
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
//桶数组
int[][] buckets = new int[10][array.length];
//每个桶的元素数量
int[] bucketSizes = new int[buckets.length];
for (int divider = 1; divider <=max ; divider *= 10) {
//将第i次的数放到桶里面去
for (int i = 0; i < array.length; i++) {
//base为n位数的数值,比如25取10位数就是(25/10%20)==2
int base = (array[i] / divider) % 10;
buckets[base][bucketSizes[base]++] = array[i];
}
//取出桶中的元素放到 原数组中去
int index = 0;
for (int i = 0; i < buckets.length; i++) {
for (int j = 0; j < bucketSizes[i]; j++) {
array[index++]=buckets[i][j];
}
bucketSizes[i] = 0;
}
}
}
public static void main(String[] args) {
int [] arrs = {500,312,110,70,25,36,245};
radixSort(arrs);
for (int arr:arrs){
System.out.println(arr);
}
}
}