如何判断排序算法是否稳定?
如果在一个待排序的序列中,存在2个相等的数,在排序后这2个数的相对位置保持不变,那么该排序算法是稳定的;否则是不稳定的。
一、冒泡排序 --稳定
1、思想:
每次遍历数组选出一个最大与后面的交换
- 外层循环:1<= i <=arr.length-1
- 内层循环:0<= j <arr.length-i
2、时间复杂度:
- 平均:O(n2)
- 最坏:O(n2)
- 最好:O(n2)
3、代码:
public void mapperSort(int[] arr){
for (int i=1;i<=arr.length-1;i++){
for (int j=0;j<arr.length-i;j++){
if (arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
4、冒泡优化:
如果已经排好序了就结束循环
public void mapperSort1(int[] arr){
for(int i=1;i<=arr.length-1;i++){
boolean isSorted=true;
for(int j=0;j<arr.length-1;j++){
if (arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
if (isSorted){
break;
}
}
}
二、选择排序--不稳定
1、思想:
- 第一步,找到数组最小的元素,把它和数组的第一个元素交换位置
- 第二步,在剩下的元素中继续寻找最小的元素,与数组的第二个元素交换位置
- 循环,直到整个数组排序完成
- 外层循环:0 <= i < arr.length-1
- 内层循环:i+1<= j <arr.length
2、时间复杂度:
- 平均:O(n2)
- 最坏:O(n2)
- 最好:O(n2)
3、代码:
public void selectSort(int[] arr){
for (int i=0;i<arr.length-1;i++){
for(int j=i+1;j<arr.length;j++){
if (arr[j]<arr[i]){
swap(arr,j,i);
}
}
}
}
三、插入排序-稳定
1、思想
- 把一个数组分成未排序和已排序两部分,每次从未排序取一个数插入到已排序的数组中
- 对于一个还未开始排序的数组,假设第一个数是已排序区域,右边是未排序区域,取右边第一个数插入左边,记录这个数值,与已排序的区域比较,比他大的右移,a[j+1]=a[j],最后找到待插入的位置,赋值给记录数值
2、时间复杂度:
- 平均:O(n2)
- 最坏:O(n2)
- 最好:O(n)
O(n):在完全有序的情况下,插入排序每个未排序区间元素只需要比较1次,所以时间复杂度是O(n)。
3、代码
public void insertSort(int[] arr){
for(int i=1;i<arr.length;i++){//待排序
int value=arr[i];
int j=0;
for(j=i-1;j>=0;j--){//已排序
if(value<arr[j]){
arr[j+1]=arr[j];//后移,腾位置
}else {
break;
}
}
//j多减了一次
arr[j+1]=value;
}
}
四、希尔排序-不稳定
1、思想:
- 希尔排序也叫”缩小增量排序“,是插入排序的一种更高效的改进版本
- 插入排序对于大规模的乱序数组效率是比较慢的,因为他每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节约时间
- 希尔排序能够以较大的步伐将小元素往前送,这样大大的减少了需要比较的次数,从而提高了速度
- 设置跳步数,gap=n/2;每隔gap个数据设为一组数据,给这一组数据完成插入排序,再让gap/=2,
直到gap=0
2、时间复杂度:
- 平均:O(n1.3)
- 最坏:O(n2)
- 最好:O(n)
3、代码:
public void shellSort(int[] arr){
int n=arr.length;
int gap=0;
for(gap=n/2;gap>0;gap/=2){
for(int i=gap;i<n;i++){//第一轮gap比较晚,比较大的数据都在后面了
int value=arr[i];
int j=i-gap;
for(j=i-gap;j>=0;j-=gap){
if (value<arr[j]){
arr[j+gap]=arr[j];
System.out.println("次");
}else {
break;
}
}
arr[j+gap]=value;
}
}
}
五、归并排序--稳定
1、思想:
归并算法的核心思想是分治法,就是将一个数组一刀切两半,递归切,直到切成单个元素,然后重新组装合并,单个元素合并成小数组,两个小数组合并成大数组,直到最终合并完成,排序完毕
分治:字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并
2、时间复杂度
- 平均:O(nlog2n)
- 最坏:O(nlog2n)
- 最好:O(nlog2n)
3、代码
public void mergeSort(int[] arr,int l,int r){
if(l<r){
int mid=(l+r)/2;
mergeSort(arr,l,mid);
mergeSort(arr,mid+1,r);
merg(arr,l,r,mid+1);
}
}
private void merg(int[] arr, int l, int r, int mid) {
int[] temp=new int[arr.length];
int i=l,j=mid,p=l;
//i:左序列指针
//j:右序列指针
//p:temp数组的指针
while (i<mid&&j<=r){//左右序列均不为空
if (arr[i]<arr[j]){
temp[p++]=arr[i++];
}else {
temp[p++]=arr[j++];
}
}
while (i<mid){
temp[p++]=arr[i++];
}
while (j<=r){
temp[p++]=arr[j++];
}
//把临时数组temp再赋值给arr数组
p=l;i=l;
while (p<=r){
arr[i++]=temp[p++];
}
}
六、快速排序--不稳定
1、思想
排序排序所采用的思想是分治思想。所谓分治,就是指以一个数为基准,将序列中的其他数往它两边扔。比它小的都扔在它的左边,比它大的都扔在它的右边,然后左边两边再分别重复这个操作,不停地分,直至分到每一个分区地基准数地左边或者右边都只剩一个数为之。
2、时间复杂度
- 平均:O(nlog2n)
- 最坏:O(n2)
- 最好:O(nlog2n)
3、步骤
- 选取基准 pivot,把所有比pivot小的数放在左边,把比pivot大的数放在右边
- 对于pivot来说,他就处在数组中的有序状态
- 对pivot左边进行一次快排
- 对pivot右边进行一次快排
4、代码
public void quickSort(int[] arr,int left,int right){
if (left<right){
int mid=getMid(arr,left,right); //第一步
quickSort(arr,left,mid); //第二步
quickSort(arr,mid+1,right); //第三步
}
}
private int getMid(int[] arr, int left, int right) {
int pivot=arr[left];
//left:检索左边,检查到比pivot大的,arr[right]=arr[left]
//right:检索右边,检查到比pivot小的,arr[left]=arr[right]
while (left<right){
while (arr[right]>pivot&&left<right){
right--;
}//出循环地条件:右边找到了比pivot小的
arr[left]=arr[right];
while (arr[left]<pivot&&left<right){
left++;
}//出循环地条件:左边找到了比pivot大的
arr[right]=arr[left];
}
arr[left]=pivot;
return left;
七、堆排序--不稳定
1、思想
我们可以把堆(以下全都默认为最大堆)看成一棵完全二叉树,但是位于堆顶的元素总是整棵树的最大值,每个子节点的值都比父节点小,由于堆要时刻保持这样的规则特性,所以一旦堆里面的数据发生变化,我们必须对堆重新进行一次构建。
既然堆顶元素永远都是整棵树中的最大值,那么我们将数据构建成堆后,只需要从堆顶取元素不就好了吗? 第一次取的元素,是否取的就是最大值?取完后把堆重新构建一下,然后再取堆顶的元素,是否取的就是第二大的值? 反复的取,取出来的数据也就是有序的数据。
2、时间复杂度
- 平均:O(nlog2n)
- 最坏:O(nlog2n)
- 最好:O(nlog2n)
3、代码
/**
* 堆排序
* @param arr 排序数组
*/
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);
}
}
/**
* 构建堆
* @param arr 数组
* @param 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);
}
}
八、计数排序--稳定
1、思想(图解)
以下以[3,5,8,2,5,4]这组数字来演示。
首先,我们找到这组数字中最大的数,也就是8,创建一个最大下标为8的空数组arr。
遍历数据,将数据的出现次数填入arr中对应的下标位置中。
遍历arr,将数据依次取出即可。
2、时间复杂度
- 平均:O(n+k)
- 最坏:O(n+k)
- 最好:O(n+k)
3、代码
public void jishuSort(int[] arr){
int max=arr[0];
for(int i=1;i<arr.length;i++){
if(arr[i]>max){
max=arr[i];
}
}
//初始化计数数组
int[] countArr=new int[max+1];
//计数
for(int i=0;i<arr.length;i++){
countArr[arr[i]]++;
arr[i]=0;
}
//排序
int index=0;
for(int i=0;i<countArr.length;i++){
while (countArr[i]-->0){
arr[index++]=i;
}
}
}
九、基数排序--稳定
1、思想
基数排序是一种非比较型整数排序算法,其原理是将数据按位数切割成不同的数字,然后按每个位数分别比较
2、时间复杂度
- 平均:O(n*k)
- 最坏:O(n*k)
- 最好:O(n*k)
3、代码
public static void sort(int[] arr)
{
int length = arr.length;
//最大值
int max = arr[0];
for(int i=0;i<length;i++){
if(arr[i] > max){
max = arr[i];
}
}
//当前排序位置
int location = 1;
//桶列表
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
//长度为10 装入余数0-9的数据
for(int i = 0; i < 10; i++){
bucketList.add(new ArrayList());
}
while(true)
{
//判断是否排完
int dd = (int)Math.pow(10,(location - 1));
if(max < dd){
break;
}
//数据入桶
for(int i = 0; i < length; i++)
{
//计算余数 放入相应的桶
int number = ((arr[i] / dd) % 10);
bucketList.get(number).add(arr[i]);
}
//写回数组
int nn = 0;
for (int i=0;i<10;i++){
int size = bucketList.get(i).size();
for(int ii = 0;ii < size;ii ++){
arr[nn++] = bucketList.get(i).get(ii);
}
bucketList.get(i).clear();
}
location++;
}
}
十、桶排序--稳定
1、思想
桶排序可以看成是计数排序的升级版,它将要排的数据分到多个有序的桶里,每个桶里的数据再单独排序,再把每个桶的数据依次取出,即可完成排序。
2、时间复杂度
- 平均:O(n+k)
- 最坏:O(n2)
- 最好:O(n)
3、代码
public static void sort(int[] arr){
//最大最小值
int max = arr[0];
int min = arr[0];
int length = arr.length;
for(int i=1; i<length; i++) {
if(arr[i] > max) {
max = arr[i];
} else if(arr[i] < min) {
min = arr[i];
}
}
//最大值和最小值的差
int diff = max - min;
//桶列表
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
for(int i = 0; i < length; i++){
bucketList.add(new ArrayList<>());
}
//每个桶的存数区间
float section = (float) diff / (float) (length - 1);
//数据入桶
for(int i = 0; i < length; i++){
//当前数除以区间得出存放桶的位置 减1后得出桶的下标
int num = (int) (arr[i] / section) - 1;
if(num < 0){
num = 0;
}
bucketList.get(num).add(arr[i]);
}
//桶内排序
for(int i = 0; i < bucketList.size(); i++){
//jdk的排序速度当然信得过
Collections.sort(bucketList.get(i));
}
//写入原数组
int index = 0;
for(ArrayList<Integer> arrayList : bucketList){
for(int value : arrayList){
arr[index] = value;
index++;
}
}
}