每日总结[10] 20191029 数据结构复习-常见8大排序

一、把我能想到的写下来+代码重写一遍

(见github的SortRewrite)

    排序分为两种:内部排序——占用内存。和外部排序——占用外部空间,如磁盘,其实外部排序的内部用的还是内部排序。

[补:元素过多,数据量过大,内存放不下时,可能需要借助外部存储器。
==线性排序O(n):桶,基数,计数]

    排序需要考虑:时间复杂度、(最好、最坏、平均)空间复杂度、稳定性(值相等的元素的前后顺序有无改变)、如何优化这四个问题。

[原先讨论时间复杂度时,数据规模n很大,表示时会忽略系数、常数、低阶,但实际软件开发中,排序的可能是10个、100个、1000个这样规模很小的数据,依次对于同一阶时间复杂度的排序算法性能对比时,需要考虑系数、常数和低阶。]

冒泡排序

    以升序为例,“冒泡”就是轻(小)的会浮上来,重(大)的会沉到底,冒泡是两两之间的比较,比如:3,7,5,1,8,4。从头开始遍历,3比7小,不变,7比5大,需要交换俩元素,数组变为3,5,7,1,8,4;7比1大,需要交换,数组变为3,5,1,7,8,4;7比8小,不变,8比4大,需要交换俩元素,数组变为3,5,1,7,4,8。第一趟下来,最大的。因为是从头往后遍历,大的给后放,所以一趟下来总会有个大的元素到它正确的位置。
[补:冒泡排序只会操作相邻的两个数据。]
代码如下:

package Java;

//冒泡排序
public class BobbleSort {
    public void sort(int[] data)
    {
        for(int i=0;i<data.length;i++)
        {
           for(int j=0;j<data.length-i-1;j++)
           {
               if(data[j]>data[j+1]) swap(data,j,j+1);
           }
        }
    }

    public void swap(int[]data,int a,int b)
    { int temp=data[a];
        data[a]=data[b];
        data[b]=temp; }
}

    时间复杂度:最好/坏 情况/平均:O(n^2)

[改:最好:要排序的数据已经是有序的了,O(n)]

    空间复杂度:没有额外开辟空间O(1).原地排序。
    稳定性:写的代码是如果data[j]>data[j+1]才交换,小于等于的情况是不交换的,因此是稳定的。
    如何优化:如果当前元素已经有序,不必再遍历,可以减少循环次数。可以设置标志位判断当前是否有序。代码如下:

package Java;

//冒泡排序
public class BobbleSort {
    public void sort(int[] data)
    {
        //优化:设置标志位,如果当前数组已有序,不需要再遍历
        for(int i=0;i<data.length;i++)
        {boolean flag=false;
           for(int j=0;j<data.length-i-1;j++)
           {
               if(data[j]>data[j+1])
               { swap(data,j,j+1);
                   flag=true;}
           }
        if(flag==false){
               System.out.println("当前数组已有序,第"+i+"个位置");
        break;
        }
    }}

    public void swap(int[]data,int a,int b)
    { int temp=data[a];
        data[a]=data[b];
        data[b]=temp; }
}

插入排序

        “插入”排序,需要联想到往一个已经有序(升序)的数组中插入元素。我们可以从后向前遍历这个有序数组,将待插入元素与数组中元素比较,如果待插入元素比已有序元素小,那么已有序元素需要往后移一位,腾出位置,如果待插入元素比已有序元素大,那么不需要再向前遍历了,待插入元素一定比前面的元素都大。这时将它放入正确的位置即可。

[补:核心思想:数据分为已排序区和未排序区。]

(1)直接插入

代码如下:

package Java;
//直接插入
//联想向一个已有序的数组插入元素
//最初数组第一个元素默认有序
public class DirectInsertition {

public void sort(int[] data)
{
    //已排序末,默认初始为0
    int already=0;
  while (already<data.length-1)
  {
      //already+1为未排序元素区首,将该元素与已排序区从后之前比较
      int j=already;
      int value=data[already+1];
      for(;j>=0;j--) {
          if (value < data[j])
              //如果未排序区首元素小于已排序区元素,已排序区元素需要往后挪一位腾出位置
          {data[j+1]=data[j];}
          else {break;}
      }
      data[j+1]=value;
      already++;
  }

}
}

时间复杂度:最好情况:O(n),(数组本身就是有序的了,内层循环只break)
最坏情况/平均:O(n^2)
[补:最坏情况:本身倒序。]
空间复杂度:没有开辟额外空间。O(1),原地排序。
稳定性:写的代码中,如果(value < data[j]),元素需要向后挪,如果(value >=data[j])是不需要挪动的,因此是稳定的。
如何优化:折半插入。

(2)折半插入

    直接插入,在寻找当前元素插入的位置时,是对已排序区从后往前遍历(这样一来,如果当前元素小于已排序区元素,已排序区元素需要向后挪一位腾位置),为提高效率,考虑折半比较,第一次取数组长度的中间,因为已经是有序数组,如果当前元素小于中间元素,说明当前元素的插入位置在中间位置的左边,反之在右边。可以设置left,right,mid。递归调用,终止条件是什么呢? 通过分析几个数组发现:要想确定插入位置,一定会经历一个left=right=mid的阶段(三个指针重合) 接下来,会发现待插入元素小于mid,(不可能是大于mid的,如果大于mid是不会落在这一区间的)right=mid-1,那么left会大于right,即left>right,这时,只需要比较mid对应的元素和待插入元素,如果mid对应的元素大于待插入元素待插入元素应放在mid的前面;反之放后面。
(注意!!!写递归终止条件时注意边界情况,比如这里的right=-1会导致返回的插入位置值为-1,因此需要单独判断.)
代码如下:

package Java.InsertSort;
//折半插入
//递归调用
public class BinaryInsertionSort {

    //传入已排序数组的末下标,返回正确位置
    public int solution(int[] data,int already)
    { int left=0;
        int right=already;
        while (true)
        {int save=data[already+1];
            int mid=(left+right)/2;
           // if(right==-1)  return mid;
                if(left>right){
                if(data[mid]<save) return mid+1;
                else  return mid-1; }
            if(data[mid]<save) left=mid+1;
            else right=mid-1;
        }}

public void sort(int[] data)
        {
            for(int i=1;i<data.length;i++)
            { int save=data[i];
                int solution=solution(data,i-1);
                for(int j=i-1;j>=solution;j--)
                {
                    data[j+1]=data[j];
                }
                data[solution]=save;
            }

        }

    }

时间复杂度:O(nlgn)
空间复杂度:O(1),没有额外开辟空间。

[改:时间、空间、稳定性均与直接插入排序相同,只是元素比较次数不同而已。]

(3)希尔排序

希尔排序又叫“缩小增量排序”,
以{1,22,8,4,2,7,18,12}为例,数组长度为8,首先增量gap=8/2=4,每次走4步,会得到{1,2},{22,7}{8,18},{4,12}四组,对这四组元素依次做插入排序,得到:{1,2},{7,22},{8,18},{4,12}(是需要在原数组上交换位置的),原数组变成{1,7,8,4,2,22,18,12}。
接下来缩小增量,gap=gap/2=2,每次走两步,得到{1,8,2,18},{7,4,22,12},对这两组元素依次做插入排序,得到:{1,2,8,18},{4,7,12,22},原数组变成{1,4,2,7,8,12,18,22}.
再缩小增量,gap=gap/2=1,每次走一步,得到即对{1,4,2,7,8,12,18,22}进行插入排序即可。

[补:可以自定义一个增量序列,不一定每次折半。]
代码如下:

package Java.InsertSort;
//希尔排序
public class ShellSort {
public void sort(int[] data)
{ int gap=data.length/2;
    while(gap>=1)
    {
        //根据普通插入排序的思想,数组第一个元素默认为初始已排序区
        //i从gap开始遍历,正好是未排序区第一个元素,
        // 仍是插入排序,注意该组元素下标的跨度即可
        for(int i=gap;i<data.length;i++)
        {
            int save=data[i];
            //遍历已排序区
            int j=i-gap;
            for(;j>=0;j=j-gap)
            {
                if(save<data[j])
                { data[j+gap]=data[j]; }
                else {break;}
            }
            data[j+gap]=save;
        } 
gap=gap/2;
    }
}
}

时间复杂度:
最好:已经是有序数组:n(lg2)
最坏:倒序数组O(nlgn)
平均:O(nlgn)
空间复杂度:没有额外空间,原地排序O(1)。
稳定性:相等情况下不操作,稳定。

[改:时间复杂度:
在这里插入图片描述
??? 不懂。
]

仍是插入排序,不过有分组,注意该组下标的跨度即可。

归并排序

归并排序,(升序)先分解,后合并
以{1,5,3,2,7,4,11,8}为例,先分解成{1,5,3,2}和{7,4,11,8},再进行分解,分解成{1,5},{3,2},{7,4},{11,8},再进行分解,分解成{1},{5},{3},{2},{7},{4},{11},{8},
分解至变成一个元素一个元素的小数组后,开始进行合并,
先将{1}和{5}合并,需要开辟一个新的空间——长度为2的数组,遍历两个小数组,并放入合并结果数组中,结果为{1,5},同理,得到结果数组{2,3},{4,7},{8,11},再进行合并,开辟长度为4的数组空间,结果为{1,2,3,5}和{4,7,8,11},再进行合并,得到{1,2,3,4,5,7,8,11}排序完成。
分解可以用递归实现。可以这样想:传入的参数是数组,left和right,比如上面的例子,left=0,right=7,先计算分解点:int mid=(0+7)/2=3,然后继续进行分解,传入
a.0和mid(3),
b.mid+1(4)和right(7),
递归调用,传入a,会得到新的mid=1,这时可以对下标为0与下标为1的元素、下标为2与下标为3的元素开始合并了,调用写好的合并方法:merge(data,left,right)。即可完成第一次合并;接下来,再递归调用分解方法,就该是终止了,此时mid变为(0+1)/2=0,这时传入的参数会是0,0和1,1因此递归的终止条件为left==right。
在合并的过程中,可以在两个数组中分别设置两个指针,比较两个数组当前元素,哪个小就把哪个放入结果数组中,如果数组遍历完毕,(一定是某个先遍历完,不会同时遍历完的。)就把另一个数组中没遍历的元素依次放入结果数组。代码可以这样设计,如果数组A的元素小于数组B的元素,就把数组A的元素放入结果数组中,此时还要考虑数组A可能已经遍历完毕。
代码如下:

package Java;

//归并排序
public class MergeSort {
    public void sort(int[] data) {
        decomposition(data,0,data.length-1);
    }

    public void decomposition(int[] data, int left, int right)
    //先分解
    {
        if(left==right) return;
        int mid = (left + right) / 2;
            decomposition(data, left, mid);
            decomposition(data, mid + 1, right);
        merge(data, left, right);}

    //合并方法
    public void merge(int[] data, int left, int right) {
        int[] newArray = new int[right-left+1];
        int mid = (left + right) / 2;
        int a = left;
        int b = mid + 1;
        int k=0;
while (k!=newArray.length)
{if(data[a]<data[b])
        {
            newArray[k++]=data[a];
            //如果左边的比较小,可能遇到左边已经遍历完毕的情况
            if(a==mid)
            { for(;b<=right;b++)
                {newArray[k++]=data[b];} }
            a++;
        }
        else
        {
            newArray[k++]=data[b];
            if(b==right)
            {for(;a<=mid;a++)
            {newArray[k++]=data[a];}
            }
            b++;
        }}



        for(int l=0;l<newArray.length;l++)
        {
            data[l+left]=newArray[l];}
    }
}

归并排序效率高的原因在于不涉及交换,比较后是存在新开辟的结果数组中的。以空间换时间???
时间复杂度:O(nlgn)

空间复杂度:O(n)额外开辟了空间。

[补:尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了,在任意时刻,CPU只会有一个函数在执行,也就只会有一个临时的内存空间在使用,临时内存空间最大也不会超过n个数据的大小。]
如何优化?

[典型题:如何在O(n)内寻找一个无序数组的第k大元素。]

选择排序

“选择”的意思是,遍历数组,从下标为0的元素开始,“选择”剩下区间中最小的元素,与初始元素交换。在“选择”的过程中,只保留下标,即找到区间中最小的元素的下标后,再与初始元素进行交换。
[补:只有索引的变化,没有交换元素。]
代码如下:

package Java;
//选择排序,升序
public class SelectionSort {
    public void sort(int[] data)
    {
        for(int i=0;i<data.length-1;i++)
        {
            int min=i;
            for(int j=i+1;j<data.length;j++)
            { if(data[j]<data[min])
            {min=j;}}
            int save=data[min];
            data[min]=data[i];
            data[i]=save;
        }
    }
}

时间复杂度:O(n^2)
空间复杂度:O(1) 没有开辟额外空间——原地排序。
稳定性,写的代码如果值相等是不进行交换的——稳定。

[改:
是不稳定的,记住一个例子{5,8,5,2,9}
]

优化:???

快速排序

快速排序和插入排序有点类似,也是有已排序区和未排序区。
普通的快排,先来个例子:以数组[0]为分区点,假设从1到i都是小于分区点的元素,从i+1到j都是大于分区点的元素,从j+1开始继续遍历到数组末,如果元素小于分区点,就将该元素于下标为i+1的元素交换,然后i++,即可保持【i表示比分区点小的区域的末元素】,如果元素大于分区点,就将j++,即可保持【j表示比分区点大的区域的末元素】。最后只需要将[i]与[0]对应的元素互换即可使原来下标为0的元素到正确的位置(因为比它小的元素都到它左边,比它大的元素都到它右边)。
那么想实现对数组的快速排序,只需要从数组[0]开始,遍历整个数组,将每个元素作为分区点,然后重复上述方法,为每个元素找到正确位置即可。代码如下:

[改:错了!!!并不是遍历数组,而是递归,继续对比它小的区间和比它大的区间进行快速排序,(怪不得我发现我的快排对值相等的情况排序存在问题。)]
[补:不能说错了。 之前有问:如果不用递归,如何实现快速排序? 哈哈哈哈我可真是太棒了。]

快速排序普通版 递归版:

package Java.QuickSort;

import java.util.Arrays;

//快速排序
public class PrimarySort {
    public void sort(int[] data)
    {
        sort1(data,0,data.length-1);
    }

    public void sort1(int[] data,int p,int r)
    {if(p>=r) return;
        int partition=findSolution(data,p,r);
        sort1(data,p,partition-1);
        sort1(data,partition+1,r);
        }
  

//错了!!!不是遍历数组为每一个元素寻找正确插入位置,而是递归调用快排
        //对比分区点小的区间进行快排,对比分区点大的区间进行快排


        public int findSolution(int[] data,int k,int m)
        {   //总是以传进小数组的第一个元素为分区点
            int pPoint = data[k];
            int i = k;
            for (int a = k+1; a <=m; a++) {
                if (data[a] < pPoint) {
                    swap(data, i + 1, a);
                    i++;
                }
             }
         swap(data,i,k);
            return i;}

        public void swap ( int[] data, int a, int b)
        {
            int temp = data[a];
            data[a] = data[b];
            data[b] = temp;
        }
    }

快速排序普通版 开空间 挨个遍历找正确位置版:
(这样写不适用于有值相等的数组)

package Java.QuickSort;
//快速排序
public class PrimarySort {
public void sort(int[] data)
{//从下标为0开始,为各元素寻找正确位置
    for(int k=0;k<data.length;k++)
    {int pPoint=data[k];
        int i=k;
        int j=k;
for(int a=k+1;a<data.length;a++) {
    if (data[a] < pPoint) {
        swap(data, i + 1, a);
        i++;
    } else {
        j++;
    }
}swap(data, i, k);  }
}

public void swap(int[]data,int a,int b)
{
    int temp=data[a];
    data[a]=data[b];
    data[b]=temp;
}
}

时间复杂度O(n^2)
[改:时间复杂度:O(nlgn) ]
空间复杂度O(1) 没有额外开辟空间。
稳定性:根据写的代码,如果值相等是不交换的,稳定。
优化:随机确定分区点,而不是第一个元素。
代码如下:

//优化:随机生成分区点
            int pPointIndex=randomly(k,m);
            swap(data,k,pPointIndex);
//优化:随机生成分区点
    public int randomly(int k,int m)
    {
        return (int)(k+Math.random()*(m-k));
    }

二路快排

刚刚的一路快排只有一个指向小于分区点末元素的指针是有用的,那个指向大于分区点末的指针其实并没有什么用。现在进行有两个指针,前面的指针i指向比分区点元素小的最后一个元素,后面的指针j指向比分区点元素大的第一个元素,两个指针同时开始移动,前面的向后走,后面的向前走,如果前面的指针指向的元素大于分区点元素,需要将这个元素与j-1指向的元素交换位置,并且j–,否则i++即可。如果后面的指针指向的元素小于分区点元素,需要将这个元素与i+1指向的元素交换位置,否则j–即可。
代码如下:

package Java.QuickSort;
//双路快排,两个指针
public class DualSort {

    public void sort(int[] data)
    {
       for(int k=0;k<data.length;k++)
       {
           int i=k;
           int j=data.length;
           int value=data[k];
           while (i+1!=j)
           {
               if(data[i+1]>value){
                 swap(data,i+1,j-1);
                 j--;
               }
               else {i++;}

               if(i+1==j) break;
               if(data[j-1]<value)
               {
                   swap(data,j-1,i+1);
                   i++;
               }
               else{j--;}
           }
           swap(data,k,i);
       }
    }

    public void swap(int[] data,int a,int b)
    {
        int temp=data[a];
        data[a]=data[b];
        data[b]=temp;
    }

}

(易错 ! ! ! )在写双路快排时我发现了一个问题,遍历数组的每个元素,每次为当前元素找到正确的位置后,这个数组是变化了的,”遍历“的并不是初始数组,而是上一次改变某些元素位置后的新的数组,因此排序后的结果是错误的。可以每次存下该元素应该去的位置,最后统一赋值。
修改后的代码:(通过单元测试)
双路快排

package Java.QuickSort;

import java.lang.reflect.Array;
import java.util.Arrays;

//双路快排,两个指针
public class DualSort {

    public void sort(int[] data) {
        int[] result = new int[data.length];
        int[] copyArray=Arrays.copyOf(data,data.length);
        int[] copyArray2;
        //为避免每次排序的都是上一次更新过的数组
        for (int j = 0; j < data.length; j++) {
            copyArray2=Arrays.copyOf(copyArray,copyArray.length);
            result[findSolution(copyArray2, j)] = data[j];
        }
        for (int k = 0; k < data.length; k++) {
            data[k] = result[k];
        }
    }

    public int findSolution(int[] data, int k) {
        swap(data, 0, k);
        int i = 0;
        int[] copyArray = Arrays.copyOf(data, data.length);
        int j = copyArray.length;
        int value = data[0];
        while (i + 1 != j) {
            if (copyArray[i + 1] > value) {
                swap(copyArray, i + 1, j - 1);
                j--;
            } else {
                i++;
            }
            if (i + 1 == j) break;
            if (copyArray[j - 1] < value) {
                swap(copyArray, j - 1, i + 1);
                i++;
            } else {
                j--;
            }

        }
        return i;
    }

    public void swap(int[] data, int a, int b) {
        int temp = data[a];
        data[a] = data[b];
        data[b] = temp;
    }
}


在单路快排递归版的启发下,写成双路快排递归版:

package Java.QuickSort;

import java.util.Arrays;

//双路快排,两个指针
//递归版
public class DualSort {

    public void sort(int []data)
    {sort1(data,0,data.length-1);}

    public void sort1(int[] data,int k,int m)
    {if(k>=m) return;
       int partition=findSolution(data,k,m);
       sort1(data,k,partition-1);
       sort1(data,partition+1,m);
    }

    public int findSolution(int[] data, int k,int m) {
        int i = k;
        int j=m+1;
        int value = data[i];
        while (i + 1 != j) {
            if (data[i + 1] > value) {
                swap(data, i + 1, j - 1);
                j--;
            } else {
                i++;
            }
            if (i + 1 == j) break;
            if (data[j - 1] <= value) {
                swap(data, j - 1, i + 1);
                i++;
            } else { j--; } }
            swap(data,i,k);
        return i;
    }

    public void swap(int[] data, int a, int b) {
        int temp = data[a];
        data[a] = data[b];
        data[b] = temp;
    }
}


堆排序

首先需要将数组变成堆,满足:每个根节点大于左子树,小于右子树。(那这个是什么数据结构??? ) 然后中序遍历堆,即可完成从小到大的排序。
[改:每个根节点大于左子树,小于右子树是二分搜索树。]
[改:]
先把数组变成堆,满足:每个根节点都不小于它的左右子树。然后依次取得堆顶,并保持堆结构。经过对每日总结[8]的回顾,需要注意以下几点:
(1)堆以数组形式存储。
(2)根大于左子树,小于右子树的是搜索二叉树,根不小于左右子树的是大顶堆。
写代码如下:(完球了,一星期前的思路已经忘光光了。从下周复习开始,每天写总结前必须复习上一次总结,难也要坚持。)
(回顾了以下上一篇发现调用了siftDown的那个heapify方法是错的哈哈哈哈,这下好了不想看了,等下次刷面经看到再总结堆排序吧。)

2.补充

通常排序的目的是快速查找。

**在相同数据集下推荐使用插入排序。**因为如果把执行一个赋值语句的时间粗略计为单位时间,冒泡排序k次交换操作,每次需要3个赋值语句,耗时3*k单位时间,而插入排序只需要k单位时间。而且插入排序的算法思路也有很大的优化空间。

在这里插入图片描述

在这里插入图片描述

3.de过 的那些bug的那些坑:

(1)排序中会遇到比较,比如插入排序,以{1,3,8,4}为例,{1,3,8}已经是有序数组,需要将元素4插入其中,从后向前遍历,4<8,需要将8向后挪一位,腾出位置,而这时的数组就变成了{1,3,8,8}如果写成if(data[j]<data[already+1]),already表示已排序区末,则already+1表示未排序区初,这时的already+1对应的元素变成了8,而不是我们未排序的那个4,因此,需要将4提前存起来,是与这个值比,而不是与那个下标对应的元素比。 特别容易错。
(2)写递归终止条件中要特别注意边界条件:<0??? >length???
比如折半插入中,如果用来标记已排序数组右端的right=-1,以{11,3}为例,其中{11}是有序数组,需要为"3"寻找位置,如果不单独判断它,从局部看,因为3<11,因此右端的right会跑到mid的左侧,变成-1,而mid=0,接下来right<left,需要确定插入位置了,因为3<11,插入位置为mid-1,即-1,出错。真是right=-1导致了返回插入数组的位置为-1,在折半插入中,比较后无非是left=mid+1,(left始终不会>already+1(最多=already+1),因此不涉及边界情况)和right=mid-1,只有right可能因为被减成-1,导致出错。
如果以{3,11}为例,不会出现这样的问题,因为执行的是left=mid+1.
(3)递归方法里面可别再"while"了,是想循环多少次 ???:

   public void decomposition(int[] data, int left, int right)
    { int mid = (left + right) / 2;
        while(mid!=left) {
            decomposition(data, left, mid);
            decomposition(data, mid + 1, right);
        merge(data, left, right);}
    }
`` `
 需要用if() return;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值