-
稳定的定义:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
-
稳定的意义:排序的内容是一个复杂对象的数字属性,且其原本的初始顺序存在意义,那么在二次排序的基础上保持原有排序,就是稳定的。比如商品销量相同,价格不同,原本已经按价格排好,现在按销量排,就要尽量避免相同销量的商品相对位置改变,要使它们价格顺序不变。
-
快速排序、希尔排序、选择排序、堆排序是不稳定的排序算法,
-
而基数排序、冒泡排序、插入排序、折半插入排序、归并排序是稳定的排序算法
(快些选一堆美女一起玩儿——不稳定)
【面试题】在某种情况下,哪种排序算法最快?
- 如果不在乎浪费空间,应该是桶排序最快
- 如果整体基本有序**(升序),插入排序最快**,如果降序,则为最慢
- 如果考虑综合情况,快速排序更加实用常见(希尔排序、堆排序等各种排序也各有优劣)
- 数据量大,数据分布比较随机,用快速排序,要求稳定则用归并。
- 冒泡排序:数据量不大,对稳定性有要求,且数据基本有序;
- 选择排序:数据量不大,且对稳定性没有要求。
一、冒泡排序算法
步骤:
(1)比较前后两个数据,若前面的大于后面的,则交换;
(2)遍历后,最大的数据“沉到了数组N-1位置
(3)最大的数据搞定后,对其它的N-1个数据进行处理:N=N-1,只要N>0,则重复前面。
最坏时间复杂度:O(n ^2),最好时间复杂度:O(n),平均时间复杂度:O(n ^2)
空间复杂度:O(1)
public class 冒泡排序
{
public static void bubbleSort(int[] a,int N)
{
int i,j;
for(i=0;i<N;i++) //全部遍历一遍
{
for(j=1;j<N-i;j++) //比较前后两个数据,自1始,到n-i结束
{
if(a[j-1]>a[j])
{
int temp;
temp = a[j-1];
a[j-1] = a[j];
a[j] = temp;
}
}
}
}
public static void main(String[] args)
{
int[] a={3,6,8,9,1,4,5};
System.out.println("按从小到大顺序排列为:");
bubbleSort(a,a.length);
for(int i=0;i<a.length;i++)
{
System.out.print(a[i] + " ");
}
}
}
二、插入排序算法
原理:将要插入的牌从右到左地比较,若原位置的牌更大,则与更前面的牌对比(原位置的后移一个),直到遇到比要插入的数小的,则要插入的数放在该位置的后面一个。
最坏时间复杂度:O(n^2),最好时间复杂度:O(n)
//插入排序法,从小到大排列
public class insertSort {
public static void main(String[] args) {
int[] a = {2,7,9,4,1,6,5};
insert(a);
for (int i : a) {
System.out.print(i+" ");;
}
}
public static int[] insert(int[] a) {
for(int i=1;i<a.length;i++) {//选中i位置的值,将其与i之前的数挨个比较
for(int j=i;j>0;j--) {
if(a[j]<a[j-1]) {
int temp;
temp = a[j-1];
a[j-1] = a[j];
a[j] = temp;
}
}
}
return a;
}
}
最佳情况:T(n) = O(n) 最坏情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
三、选择排序
原理:从数组中选择最小的元素,与第一个元素交换位置;再从剩余数组中选择最小的,与第二个元素交换位置,直到实现从小到大排列。
public class SelectionSort {
public static void main(String[] args) {
int[] a = {2,6,8,4,1,5,3};
Selection(a);
for (int i : a) {
System.out.print(i+" ");
}
}
public static void Selection(int[] a) {
//寻找最小值
for(int i=0;i<7-1;i++) {
int min = i;
for(int j=i+1;j<7;j++) {//寻找当前数组的最小元素下标
if(a[j]<a[min]) {
min=j;
}
}
swap(a,min, i);
}
}
//——Java对普通类型的变量是不支持引用传递的,这里需借用数组来实现交换!
public static void swap(int[] a, int i,int j) {
int temp = a[j];
a[j] = a[i];
a[i] = temp;
}
}
四、快速排序算法(*)
原理:
1、先从数列中取出一个数作为基准数;
2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边;——二分
3、再对左右区间重复第二步,直到各区间只有一个数,此时数组有序。
1)设置两个变量i、j,排序开始的时候:i=0,j=n-1;
2)第一个数组值作为比较值,首先保存到temp中,即temp=A[0];
3)然后j-- ,向前搜索,找到小于temp的数后,停下来;
4)然后i++,向后搜索,找到大于temp的数后,停下来, 交换:s[j]=s[i]
5)继续重复第3、4步,直到i=j,最后使得s[i]=temp
6) 然后采用“二分”的思想,以i为分界线,拆分成两个数组 s[0,i-1]、s[i+1,n-1]又开始进行一遍上述排序
具体步骤:
图片来源于:https://blog.csdn.net/qq_40941722/article/details/94396010
(1)以i=0,j=n-1分别指向序列两端,以A[0]=temp作为基准值;
(2)j–从后往前搜索,找到比基准值小的后停下来,i++从前向后搜索,找到比基准值大的停下来
(3)交换A[i]=A[j]
(4)j和i继续移动(j先移动),则交换9和4,得到如下:
(5)直到i=j后,使A[i]=A[j]=基准值,此时基准值左边的数都比它小,右边的数都比它大,对于基准值来说,左右两边就有序了。
(6)采用“二分”的思想,重新选取基准值,使用递归的方法,对左右两边的序列重复上述排序:
(7)得到:
最终得到有序序列:
其完全具体步骤如图:
最坏时间复杂度:O(n^2),平均时间复杂度:O(nlogn)
空间复杂度:O(nlogn)
import java.util.*;
public class Main {
public static void main(String[] args) {
int[] a = {6,1,2,7,9,3,4,5,10,8};
quickSort(a, 0, a.length - 1);
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
System.out.println();
//System.out.print(Arrays.toString(a));
}
public static void quickSort(int[] a,int low,int high){
if(low > high){
return;
}
int i = low;
int j = high;
int key = a[low];//基准值
while(i < j){//循环至i=j
//(1)向前搜索,找到比基准值小的,停下来
while(a[j] >= key && i < j){
j--;
}
//(2)向后搜索,找到比基准值大的,停下来
while(a[i] <= key && i < j){
i++;
}
//(3)此时交换两者
if(i < j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
//(4)此时i=j,将此处的数a[i]=a[j]和基准值key交换
a[low] = a[i];
a[i] = key;
//(5)二分,左右分别递归
quickSort(a, low, i - 1);
quickSort(a, j + 1, high);
}
}
4.1 利用快排思想,找出第K大的数
import java.util.*;
public class Finder {
public int findKth(int[] a, int n, int K) {
// write code here
return findK(a, 0, n-1, K);
}
public static int partition(int[] arr, int left, int right) {
int pivot = arr[left];
while (left < right) {
while (left < right && arr[right] <= pivot) {
right--;
}
arr[left] = arr[right];
while (left < right && arr[left] >= pivot) {
left++;
}
arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}
public static int findK(int[] arr, int left, int right, int k) {
if (left <= right) {
int pivot = partition(arr, left, right);
if (pivot == k - 1) {
return arr[pivot];
} else if (pivot < k - 1) {
return findK(arr, pivot + 1, right, k);
} else {
return findK(arr, left, pivot - 1, k);
}
}
return -1;
}
}
五、归并排序(*)
原理:归并(递归、层层合并)排序法(Merge)即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
——分治法,各层分治递归
【时间复杂度】:归并排序最好、最差和平均时间复杂度都是O(n*logn),因为每次划分时两个子序列的长度基本一样。
//归并排序法,从小到大排列.
//把待排序序列分为若干个子序列,每个子序列是有序的。
//然后再把有序子序列合并为整体有序序列。
public class 练习题1_1
{
public static void mergeSort(int[] a)
{
sort(a,0,a.length-1);
}
public static void sort(int[] a, int left, int right)
{
// left:左数组第一个元素索引;right:右数组最后一个元素索引
if(left>=right) return;//递归边界条件
int center = (left + right)/2;//中间索引,划分子序列
sort(a,left,center);//左边递归
sort(a,center+1,right);//右边递归
merge(a,left,center,right);//合并
}
//对两个已经有序的数组进行归并
public static void merge(int[] a,int left,int center,int right)
{
int[] tempArr = new int[a.length];//临时数组
int mid = center +1;//右边第一个元素索引
int third = left;//记录临时数组的索引
int temp = left;//缓存左边第一个元素的索引
while(left<=center && mid <= right)
{
//从两个数组中取出最小的放入临时数组中
if(a[left]<=a[mid])
{
tempArr[third++] = a[left++];
}
else
{
tempArr[third++] = a[mid++];
}
}
//剩余部分依次放入临时数组
while(mid<=right)
{
tempArr[third++] = a[mid++];
}
while(left <= center)
{
tempArr[third++] = a[left++];
}
//将临时数组中的内容拷贝回原数组中
while(temp <= right)
{
a[temp]=tempArr[temp++];
}
}
}
六、堆排序
大顶堆与小顶堆:
大顶堆:每个结点都大于其左右孩子结点
小顶堆:每个结点都小于其左右孩子结点
升序:大顶堆(因为顶部大的先出去)
降序:小顶堆
堆排序基本步骤:
步骤一:构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆);
步骤二:最大值为根节点,将堆顶与末尾元素进行交换,使末尾元素最大,然后继续调整堆(将剩下的n-1个元素重新构建成大顶堆),再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换,直到整个序列有序。
具体步骤:
步骤一:构造初始堆:
-
初始元素为:[4,6,8,5,9],
-
从最后一个非叶子结点开始:6,从左至右,从上至下进行调整(与其叶子结点进行对比,交换);
3)找到第二个非叶子结点:4,进行调整:
4)再次调整最后一个非叶子结点
此时,无序序列就被构造成一个大顶堆了。
步骤二:最大值为根节点,将堆顶与末尾元素进行交换,使末尾元素最大
非叶子结点的索引:arr.length/2-1
package erchashu;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr = {4,6,8,5,9,1,23,15,7};//要求将该数组升序排列
heapSort(arr);
}
//堆排序方法
public static void heapSort(int[] arr) {
System.out.println("堆排序:");
//构建第一个大顶堆
for(int i = arr.length/2-1;i>=0;i--) {//i=arr.length/2-1
adjustHeap(arr,i,arr.length);
}
System.out.println("最初调整得到大顶堆数组="+Arrays.toString(arr));
//头尾交换
for(int j=arr.length-1; j>0 ;j--) {
//交换大顶堆顶部和末尾元素
int temp = arr[j];
arr[j]=arr[0];
arr[0] = temp;
//再调成大顶堆
adjustHeap(arr, 0, j);
}
System.out.println("最终结果="+Arrays.toString(arr));
}
//将二叉树调整成大顶堆:将以 i 对应非叶子结点的树调整为大顶堆
//i:非叶子结点在数组中的索引,length:对多少个元素进行调整(逐渐减少)
public static void adjustHeap(int[] arr,int i,int length) {
int temp = arr[i];//先取出当前非叶子结点元素的值,保存在临时变量中
//i为要调的非叶子结点,k为其左子结点
for(int k = i * 2 + 1;k < length;k = k * 2 + 1) {
if(k+1 < length && arr[k] < arr[k+1]) {//判断左右子节点的大小
k++;//如果右子结点更大,则让k指向右子结点
}
if(arr[k]>temp) {//子结点大于父节点
arr[i]=arr[k];
i = k;//难点:i指向k,继续循环比较
}else {
break;
}
}
//for循环结束后,此时已经将以i为父结点的局部树的最大值调整到了顶部
arr[i]=temp;//将父结点放到调整后的位置
}
}
时间复杂度全是:O(nlogn)
6.1 利用大顶堆找出第K大的数
class Solution {
public int findKthLargest(int[] nums, int k) {
return heapSort(nums, k);
}
private int heapSort(int[] nums, int k) {
//将无序数组构成一个大顶堆
for (int i = nums.length / 2 - 1; i >= 0; i--) {
adjustHeap(nums, i, nums.length);
}
int count = 0;
for (int j = nums.length - 1; j >= 0; j--) {
count++;
int temp = nums[j];
nums[j] = nums[0];
if (count == k) {
return nums[0];
}
nums[0] = temp;
adjustHeap(nums, 0, j);
}
return 0;
}
private void adjustHeap(int[] nums, int i, int length) {
int temp = nums[i];
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && nums[k + 1] > nums[k]) {
k++;
}
if (nums[k] > temp) {
nums[i] = nums[k];
i = k;
} else {
break;
}
}
nums[i] = temp;
}
}