一.算法分类
第一类 原始
最简单又是最复杂:插入排序,冒泡排序,选择排序,思路最简单,但是时间复杂度是最大的o(n2),空间复杂度倒是还好,是O(1)
第二类 进阶
借助了空间或者数据结构,降低了时间复杂度的算法:快速排序,归并排序,堆排序,希尔排序,时间复杂度都是O(nlogn),空间复杂度不一定
第三类 特殊
针对于特殊的数据使用的;基数排序和桶排序
二.Java实现
2.1.1原始-直接插入排序
思路:
将第i个元素插入0-i-1个元素中的合适位置,并将该位置以后的每一个元素都向后挪动一个位置,使得前i个元素是有序的。用这种方式使元素从第2个开始到第n个都变成有序的
代码:
public void insertSort(int[] a ){
int n= a.length();
int k = 0;
int temp;
for(int i =1;i<n;i++){//从第2个元素a[1]开始到第n个元素a[n-1]
for(int j = 0;j<i;j++){//在前i-1个元素中找位置
if(a[j]>a[i])
{//比较当前元素a[i]与前面的元素的大小
k=j;//记录下第一个比a[i]大的下标,这是a[i]要插入的位置
temp = a[i];//记录原来的a[i]
//将a[k]后面的元素都后移一格
for(k=i;k>j;k--)
{
a[k]=a[k-1];
}
a[j]=temp;//该放的位置放上原来的a[i]
break;//退出此次循环,前i个元素已经有序
}
}
}
}
时间复杂度:两层循环,必然是O(n2),第一层循环代表进行n次插入排序,第二次循环代表每插入排序需要进行移动的次数
空间复杂度: 没用到啥,就是O(1)
稳定性分析:不稳定,比如两个值都是k,第二个的k会插到第一个k的前面
2.1.2 原始-冒泡排序
思路:
通过左右元素比较的方式,将大的元素放到右边,慢慢的大的元素就会浮到右边,第一轮将第一大的元素放到最右边,第二轮将第二大的元素放到右边倒数第二个…这样一共进行n-1轮之后就会使前n-1大的元素都就位了,n个元素自然就都有序了
代码:
public void bubbleSort(int[] a ){
int n = a.length();
int temp;
for(int i =0;i<n-1;i++)
{//进行n-1趟冒泡
//第i趟的时候,后面的i个元素位置都定好了,只需要对前面的n-i个元素冒泡
for(int j = 0;j<n-i;j++)
{
if(a[j]>a[j+1])//与右边的元素比大小,如果当前的元素比右边的大,就交换
{
temp = a[j];
a[j] = a[j+1];
a[j=1] = temp;
}
}
}
}
时间复杂度: 两个for循环,立即推是O(n2)
空间复杂度:没用到啥,就是O(1)
稳定性分析:绝对稳定,每一轮都有一个元素的位置定得死死的
2.1.3 原始-选择排序
思路:
第一轮选择最小的元素,与第一个元素交换,第一轮选择最小的元素,与第一个元素交换,这样一共进行n-1轮之后就会使前n-1小的元素都就位了,n个元素自然就都有序了(也可以先选大的元素和右边的交换)
代码:
public void chooseSort(int[] a ){
int n = a.length();
int temp ;
int min;
int minIndex ;
//进行n-1次选择与交换,也可以理解为为a[0]-a[n-1]的元素找位置
for(int i =0;i<n-1;i++)
{
min = 256;
minIndex = i;
//位置i,放的是位置i到位置n里面最小的元素(a[i]--a[n]中最小的)
for(int j = i;j<n;j++)
{
if(a[j]<min)//找更小的,一直比较,找到头就能找到最小的
{
//找到了更小的就记录
min= a[j];
minIndex = j;
}
}
//找到了最小的元素之后,将a[i]和a[minIndex]交换
if(i!=minIndex){//下标不相同才交换,否则就自己换自己了
temp = a[i];
a[i] = min;
a[minIndex] = temp;
}
}
}
时间复杂度: 两个for循环,立即推是O(n2)
空间复杂度:也没用到啥,就是O(1)
稳定性分析:先举例子再说答案,比如两个值都是k,如果第二个的k选位置的时候,第一个k位置早就选好了,那就稳了(比如 1,7,2,8,2); 但是如果在第一个k选位置之前,被最小的数替换到了第二个k的后面就再也不动了,这样就不稳(比如 2,2,1,3),结论是不稳的
2.2.1 进阶-快速排序
思路:
随机选一个元素为参考元素(简单起见可以是第一个),然后把所有小于它的元素放到它的左边 ,大于它的元素放到他的右边,,这个移动可以通过交换的方式进行,然后再递归,分别对两边的元素快速排序。
代码:
public void fastSort(int[] a,int h,int t){//h,t分别是头,尾元素的下标
int m = a[h];//取第一个元素为参考元素,m是参考元素的值
int i = h;int j = t;//设置i,j两个指针
int temp;
int mid ;//一轮之后参考元素的位置
while(i<j)//指针不重合之前
{
//每一轮都有一个指针指向的元素等于m,所以只有一个指针在走
while(a[j]>m)//a[j]比参考元素大,把它放在右边不管,继续向左找下去
{
j--;
}
a[j]=m;a[i]=a[j]
while(a[i]>m)//a[i]比参考元素小则把它放在左边不管,继续向右找下去
{
i++;
}
//都定下来之后交换
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//结束之后,i,j相等,得到min
min = i;
//递归对两边使用
fastSort(a,h,min)
fastSort(a,min+1,t)
}
public void useFastSort(int[]a)
{
fastSort(a,0,a.length-1);
}
这是我写的第一个思路,后来发现是有破绽的,当遇到一个与m相等的元素时,i,j都不走了,出现死锁。
所以来看第二种实现(只是while循环不同):
while(i<j)//指针不重合之前
{
while(a[j]>=m)//a[j]比参考元素大或者相等,把它放在右边不管,继续向左找下去
{
j--;
}
a[i]=a[j];//找到之后把a[i]替换成a[j],现在j位置上的元素已经在i那里有一份了,j位置可以看成没有用的了,接下来可以装别的元素,而第一轮的a[i]是m,是已经被记录下来的,也可以看做没有用了,就装其他的元素
i++;
while(a[i]>=m)//a[i]比参考元素小则把它放在左边不管,继续向右找下去
{
i++;
}
a[j]=a[i];
j--;
}
//一轮结束了之后,i=j,这个位置上的元素自然也是重复过的,但少了参考元素,现在把参考元素放进来
a[i]=m;
时间复杂度: 每一轮的时间复杂度是0(n),要进行几轮就看有几次递归。要进行几轮递归呢?我们可以换个角度看:如果把这个数组看成是树,递归是对左右子数的递归的话,这棵树的深度就是递归的次数,深度最小的情况是这棵树是完全二叉树,深度是log2n,深度最大的情况是这棵树每层只有一个节点(所有有子数的节点只有右节点或只有左节点,一字排开),所以时间复杂度综合来看是O(nlogn);
空间复杂度:递归用到了栈,深度就是上面分析的树的高度,就是O(logn)
稳定性分析:肯定不稳定,一轮下来都稳定不了,后面的小的元素很容易就可以到前面的小的元素的左边,比如3,4,1,2,1,一轮之后1,2,1, 3,4
2.2.2 进阶-归并排序
思路:
一组一组地排,先一组两个(可能有落单的),组内排好序,然后在两个相邻的组合并变成一组四个元素(有一组可能不够四个),组内排好序,然后再合并…这样下去直到最大的组是整个数组,这个其实也是通过递归来做的,通过由大变小最后处理小模块的递归,有两个过程,一个是合并后排序,还有一个过程是递归过程。
代码:
public void mergeSort(int[] a,int h,int t)
{
if(h<t){//先分别递归排好序再合并(如果只有两个元素,到后面会直接执行merge())
int mid = h+(t-h)/2;
mergeSort(a,h,mid);
mergeSort(a,mid+1,t);
merge(a,h,mid,t);//合并
}
}
public void merge(int[] a,int low,int mid,int high)//合并加排序
{
//一个临时数组来存
int [] temp = new int[high-low+1];
//两个指针
int p1 = low;
int p2 =mid+1;
int k = 0;//temp数组的下标
while(p1<=mid&&p2<=high)
{
if(a[p1]>=a[p2])
{
temp[k++] = a[p2++]//小的进来
}else
{
temp[k++] = a[p1++]//小的进来
}
}
//把提前结束的半边剩下的有序部分接在后面
while(p1<min)
{
temp[k++] = a[p1++];
}
while(p2<high)
{
temp[k++] = a[p2++];
}
//最后把temp数组的东西装回去,注意a是从下标为low的地方开始装
for(int i = 0;i<temp.length,i++)
{
a[low+i] = temp[i];
}
}
时间复杂度: 合并的过程是O(n),递归的过程是O(logn)
空间复杂度:递归过程用了栈,复杂度是O(logn)
稳定性分析:稳定的,按上面的写法,元素相等时,总是先取前面组的元素放入temp,这样可以实现相邻两组的相等的元素的相对位置不变。在这样的排序是稳定的。
2.2.3进阶-堆排序
思路:
利用大根堆的思想,不断地取出根元素,递归到最后可以实现有序。具体做法是:
step1建堆: 先建立一个元素个数为n的大根堆,此时堆顶元素是最大的,
step2交换: 把堆顶与堆尾元素交换,那么此时获得了最大的元素,放在了最后,而前n-1个元素组成的堆又不是大根堆了,要进行调整
step3调整: 把这n-1个元素的堆调整成大根堆,此时堆顶是这n-1个元素里面最大的,同时也是所有元素里第二大的,再次把这个第二大与堆尾元素(倒数第二个)交换,此时最后两个元素都找到了自己的位置
重复以上步骤,直到堆的元素只剩下1个。
需要的函数是:一个包括了所有步骤的元素,一个建立堆的元素,一个调整的元素,还有三个寻找父节点,子节点的计算函数;
代码:
// 堆排序,总指挥部,调用其他函数完成了所有的步骤
public void heapSort(double[] a) {
heapSize = a.length;
// 建立大根堆
buildMaxHeap(a);
//交换,缩小无序区,调整
for (int i = a.length - 1; i > 0; i--) {
double temp = a[i];
a[i] = a[0];
a[0] = temp;
heapSize--;//缩小无序区
maxHeapity(0);//通过调整的方式获得大根堆而不是调用buildMaxHeap()
}
}
}
先写找父子节点的辅助函数:
// 找出函数的的根节点,左孩子,右孩子
protected int parent(int i) {
return (i - 1) / 2;
}
protected int left(int i) {
return 2 * i + 1;
}
protected int right(int i) {
return 2 * i + 2;
}
}
建立大根堆:
// 建立大根堆
public void buildMaxHeap(double[] a) {
heapSize = a.length;
for (int i = parent(heapSize - 1); i >= 0; i--) {
maxHeapity(i);// 从下而上的调整
}
}
调整,调整自己与孩子的
// 进行调整
public void maxHeapity(int i) {
int l=left(i);
int r=right(i);
int largest=i;
if(l<=heapSize-1&&a[l]>a[i]) {//有左孩子且比根节点大
largest=l;
}
if(r<=heapSize-1&&a[r]>a[largest]) {//有右孩子来挑战最大元素
largest=r;
}
if(largest!=i) {//如果最大的是孩子不是自己,则需要调整这个结构并对调整后的大孩子进行调整
double temp=a[i];
a[i]=a[largest];
a[largest]=temp;
maxHeapity(largest);//对调整后的大孩子进行调整
}
}
时间复杂度: 调整n次,每一次调整的次数是树的高度logn,所有时间复杂度是O(nlogn)
空间复杂度:调整的时候用到了递归,利用了栈,递归次数是栈的高度,所以是O(logn)
稳定性分析:不稳定,最简单的例子:1,5,5,第一轮之后:1,5,5,右孩子与根相等时不调整,那么堆顶先输出的被放在了后面,右孩子与根的相对位置就乱了。
2.2.4 进阶-希尔排序
思路:
可以理解成步长更大的插入排序,这与直接插入排序比优势在于,直接插入排序的步长是1,每一次需要移动的元素都很多,总的次数大,而步长越大,每次移动的距离越大,次数越少,尽管希尔排序的步长会慢慢减小,但数据越来越趋于基本有序,后面需要移动的情况就会减少很多,数据量越多,希尔排序的优势就越明显。
这个过程中,步长也很重要,会直接影响效率。那么步长怎么定呢?研究表明,步长h值最初等于1,然后用公式h=3*h+1,得到1,4,13,40,121,364…这个魔法序列能让效率到达最高。但是这样不好写惹。
在希尔的原稿中,他建议初始的间距为N/2,简单地把每一趟排序分成了两半是步长的最好选择(引用来自https://www.cnblogs.com/jsgnadsj/p/3458054.html)
那我们就定得简单粗暴一点,一半一半地选吧
代码:
public void shellSort(int[] a){
int length = a.length;
for(int gap = length/2;gap>0;gap/=2)//可以看成一共有gap组
{
for(int i = gap;i<length;i++)//取小组内的第二个元素a[i]
{
int temp =a[i];//记录这个元素
//从后往前找,没办法从前往后,因为不知道c偏移量
int k= i-gap
//由于是从后往前,所以只能边比较边移动,不能找到了位置再移动
while(k>=0&&a[k]>temp)
{ //比它大的元素都要移动
a[k+gap] = a[k];
k-= gap;
}
//找到下一个比a[i]小的元素a[k]就停下
//而a[k+gap]是一个已经移动过的值,也是temp应该放入的位置
a[k+gap] = temp;
}
}
}
时间复杂度: 执行次数依赖于增量序列,上面的这个方法一共进行了logn次直接插入排序,每次插入排序的时间复杂度来源于
for(int i = gap;i<length;i++)
所以插入排序的时间复杂度是O(n),所以希尔排序的复杂度是O(nlogn)。
空间复杂度:也没用到啥,就是O(1)
稳定性分析:不稳定,直接插入都不稳定这个更不可能稳定了。
2.3 特殊-桶式基数排序
思路:
每个值都建立一个桶,根据数值将不同的数放入桶中然后依次倒出,一般来说是建立10个桶,分别是0-9,然后先对个位数进行排序,然后对十位数进行排序…慢慢升高。有多少位数就进行多少次桶排,用一个bucket数组来记录每个桶里面有多少数值,最后倒出的时候可以知道桶里面的元素在哪个位置
radix是基数,也是桶的个数,最常见的是基数是10,代表0-9,d是数组里面的最高位数字,最高位是几,就进行几轮桶排,比如最大的数是999,是三位数,就进行三轮排序
代码:
public void radixSort(int[] data, int radix, int d) {
// 缓存数组
int[] tmp = new int[data.length];
// buckets用于记录待排序元素的信息
// buckets数组定义了max-min个桶
int[] buckets = new int[radix];
for (int i = 0, rate = 1; i < d; i++) {
// 重置count数组,开始统计下一个关键字
Arrays.fill(buckets, 0);
// 将data中的元素完全复制到tmp数组中
System.arraycopy(data, 0, tmp, 0, data.length);
// 计算每个待排序数据的子关键字
for (int j = 0; j < data.length; j++) {
int subKey = (tmp[j] / rate) % radix;
buckets[subKey]++;
}
//记录这个桶在数组的第几位结束,这样我们可以知道第j个桶在数组中下标是
//从bucket[j-1]到bucket[j]+buckets[j - 1];
for (int j = 1; j < radix; j++) {
//从1开始,因为第一个桶的元素在数组中
//是从0到bucket[1];
buckets[j] = buckets[j] + buckets[j - 1];
}
// 按子关键字对指定的数据进行排序
//从后往前放,先放大的再放小的,因为这样就比较好放了
for (int m = data.length - 1; m >= 0; m--) {
int subKey = (tmp[m] / rate) % radix;
data[--buckets[subKey]] = tmp[m];
}
rate *= radix;//这个是模,可以实现第一次处理个位,第二次处理十位
}
}
时间复杂度: 执行d*(n+radix+n)次,所以时间复杂度是O(n)
空间复杂度:用到桶了,就是O(M),M是桶的个数
稳定性分析:稳定的,都是从头开始,如果遇到相等的值也不会打乱次序