冒泡排序:
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,
若发现逆序则交换,使值较大的元素逐渐从前移向后部
冒泡排序有两层循环,
外层循环来控制排的次数(比如九个数,排八次就一定能全部排好)
内层循环将大的数字往后排。
//升序排序
public void bubbling(int[] arr){
System.out.println("排序之前:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
int t = 0;
for(int i=0;i<arr.length;++i){
for(int j=0;j<arr.length-1;++j){
if(arr[j]>arr[j+1]){
t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
System.out.println(Arrays.toString(arr));
}
System.out.println();
System.out.println("排序之后:");
for(int temp:arr){
System.out.print(temp+" ");
}
}
冒泡排序就是把数组中大的数字往后排。下面是优化版本:
public void bubbling(int[] arr){
System.out.println("排序之前:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
int t = 0;
boolean flag = false;
for(int i=0;i<arr.length-1;++i){
for(int j=0;j<arr.length-1-i;++j){
if(arr[j]>arr[j+1]){
flag = true;
t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
if(!flag){
break;
}else {
flag = false;
}
System.out.println(Arrays.toString(arr));
}
System.out.println();
System.out.println("排序之后:");
for(int temp:arr){
System.out.print(temp+" ");
}
}
优化的主要有两个点:
1:对于两层for循环的终止条件,当我们每排一次之后,其实最后一个位置的数字已经是确定好了的,我们可以不用去管,所以: j<arr.length-1-i
2:增加了flag标志,这个很容易理解,看之前的结果,其实数组已经排好了,不过因为i和j还没到终止条件,所以循环还在接着做,我们设置一个flag,如果数组中没有发生任何交换,我们就break。节省开销。
冒泡排序的效率:
时间复杂度:O(n2)
空间复杂度:O(1)
选择排序:
选择排序算法是通过遍历数组,选择出数组的最小或最大值,与指定位置交换数据,遍历完整个数组的所有位置就完成排序
public void select(int[] arr){
System.out.println("排序之前:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
int min = 0;
int i,j;
for(i=0;i<arr.length-1;++i){
min = i;
for(j=i+1;j<arr.length;++j){
if(arr[min]>arr[j]){
min = j;
}
}
swap(arr,min,i);
System.out.println(Arrays.toString(arr));
}
System.out.println("排序之后:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
}
选择排序算法思路就很简单,就是遍历一次找到数组中的最小值,然后把这个最小值放到正确的位置上就行。
选择排序的效率:
时间复杂度:O(n2)
空间复杂度:O(1)
插入排序:
这里的插入排序一般是指直接插入排序。
插入排序的思想是:每一趟将一个待排序的记录,按其关键字大小插入到已经排好序的一组记录的适当位置上。(例如打扑克牌的时候,每摸一张牌,就把这张牌放到合适的位置上)
public void insert(int[] arr){
int t = 0;
int i,j;
System.out.println("排序之前:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
for(i=1;i<arr.length;++i){
t = arr[i];
for(j=i;j>0&&t<arr[j-1];j--){
arr[j] = arr[j-1];
}
arr[j] = t;
System.out.println(Arrays.toString(arr));
}
System.out.println("排序之后:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
}
i从1开始遍历,t = 5;
然后t和前一个数字(8)进行比较,然后发现t<8,说明要把这个5插入到8的前面
temp = 3
8 5 4 2 7 9 1 3 6 -> 8 8 4 2 7 9 1 3 6 -> 5 8 4 2 7 9 1 3 6
后面的过程类似。
插入排序的效率:
时间复杂度:O(n2)
空间复杂度:O(1)
希尔排序:
基本思想:希尔排序是把序列按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量的逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰好被分为一组,算法便终止。
希尔排序一般步骤:
1:选择增量序列:一般是和数组长度有关系:len/2,len/4......;
2:对每个子序列进行插入排序;
3:逐步缩小增量;
4:缩小到最后增量就为1,这个时候就完全变成了直接插入排序
就比如:{8,5,4,2,7,9,1,3,6}
一开始的时候增量一般是len/2,所以上面这个数组就会被分成四个组:
{8,7,6},{5,9},{4,1},{2,3},然后对每个小组进行分别排列
得到{6,7,8},{5,9},{1,4},{2,3}。
然后增量缩小,变成len/4 = 2;就变成:
{6,7,8,5,9},{1,4,2,3}。然后就继续排
当增量变成一时,就变成了一个直接插入排序。
public void shell(int[] arr){
int len = arr.length;
int gap = 0;
int i,j;
System.out.println("排序之前:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
for(gap = len/2;gap>0;gap=gap/2){//外面这层循环是控制增量的。
for(i=gap;i<arr.length;++i){
int t = arr[i];
for(j=i;j>=gap&&t<arr[j-gap];j-=gap){
arr[j] = arr[j-gap];
}
arr[j] = t;
}
System.out.println(Arrays.toString(arr));
}
System.out.println("排序之后:");
for(int temp:arr){
System.out.print(temp+" ");
}
}
拿增量为4(len/2)的例子来说明一下:
8 5 4 2 7 9 1 3 6
一开始i=gap,arr[i] = 7,{7,8}做了一次插入排序:数组就变成:
7 5 4 2 8 9 1 3 6
然后i++;
arr[i] = 9 ,{5,9}也排序了,不过数组没有改变:
7 5 4 2 8 9 1 3 6
然后i++;
arr[i] = 1,{4,1}分到了一组进行排序,数组就变成:
7 5 1 2 8 9 4 3 6
然后i++,{2,3}的顺序是对的,所以数组没有改变
然后i++,指向了最后一个元素,这个时候有一点点不同,
因为我开始说的时候是将{8,7,6}分到一组,但因为遍历的顺序是从左往右的,所以,一开始,6并没有和7,8进行比较,是到了最后i==len-1的时候,才进行比较,6就插入到7,8的前面了:
数组就变成:6, 5, 1, 2, 7, 9, 4, 3, 8。
以上就是增量为len/2时候的排序过程,代码看着有三个for循环,仔细拆解来看
第一个for循环就是控制增量的,第二个和第三个就是插入排序的处理。其实和插入排序对比起来看,插入排序的增量是1,所以j的变化是j--,在希尔排序中,有不同的增量,
所以j的变化是j-=gap。
希尔排序的效率:
时间复杂度:O(logn)~ O(n2)(主要取决于增量的选择)
空间复杂度:O(1)
希尔排序的应用场景:
1:对于需要排序的数据量较大的数组或列表,希尔排序可以比插入排序更快地将数据排序。
2:希尔排序可以作为其他排序算法的预处理步骤,例如在快速排序和归并排序中,可以使用希尔排序对数据进行预处理,以提高排序效率。
3:希尔排序可以用于对链表进行排序,由于链表的特殊性质,插入排序的效率较低,而希尔排序可以通过对链表的节点进行分组来提高排序效率。
归并排序:
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
主要从两个步骤要阐述一下这个归并排序把:一个方面是分,一个方面是治。
分:
分就是将数组进行不断的拆分,直到拆分到不能再拆了为止。这个步骤的话主要是通过递归来实现,我们只需要控制好下标(left和right)就好了。
if(left==right){//这个就说明已经不能再拆了
return ;
}
int mid = (right+left)>>1;
merging(arr,left,mid,temp);
merging(arr,mid+1,right,temp);
int i,j;
for(i=left;i<=right;++i){
temp[i] = arr[i];
}
治:
治就是当我们把这个数组拆成不能再拆的时候,我们这个时候就要向上进行整合了。
那这个具体怎么整合呢,仔细思考之后就可以知道,我们先上整合就是在合并两个有序的数组
这里我推荐两道题目:
这两道题运用的思路就是归并排序中的治的思路。(不过还是我最后的代码写出来还是略有不同)
int i,j;
for(i=left;i<=right;++i){
temp[i] = arr[i];
}
i = left;
j = mid+1;
for(int k=left;k<=right;++k){
if(i==mid+1){
arr[k] = temp[j];
j++;
}else if(j==right+1){
arr[k] = temp[i];
i++;
}else if(temp[i]<temp[j]){
arr[k] = temp[i];
i++;
}else if(temp[i]>temp[j]){
arr[k] = temp[j];
j++;
}
}
分析:我是先将两个有序数组存到一个数组temp中,然后通过下标来将两个有序数组合并
举个例子把:我们就拿整合最后一层递归来举例把:就是图中的{4,5,7,8}和{1,2,3,6}整合成{1,2,3,4,5,6,7,8}来举例
我将i = left指向4,j=mid+1指向1,然后我开始遍历,我先解释我循环中的后两个else-if,那两个肯定好理解,如果temp[i]>temp[j],说明我需要把temp[j]插入到最后的arr数组中,另一个else-if也是同样的道理。
那剩下两个判断语句是什么意思呢,可以设想一个当{1,2,3,6}的所有元素都已经放到arr数组中去了,可是{4,5,7,8},这两个数组却还有两个数没有放进去,那这个时候,我们就可以不用再进行比较了,我们直接把{7,8}放到arr中去就行,那再想一下,当{1,2,3,6}所有数组都排完了之后,j是不是就等于right了,同样道理,如果是前面那一个数组先排完,那i就等于mid+1了。
如果有对比我上面推荐的两道leetcode的话,我在左这两道题目的时候,我是用了下面两种方式判断。其实本质就是换汤不换药。
if(i<m){
while(i<m){
sorted[cnt++] = nums1[i++];
}
}
if(j<n){
while(j<n){
sorted[cnt++] = nums2[j++];
}
}
if(cur1!=null){
cur.next = cur1;
}
if(cur2!=null){
cur.next = cur2;
}
贴一个完整的归并排序的代码:
public void merger(int[] arr){
System.out.println("排序之前:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
int[] temp = new int[arr.length];
merging(arr,0,arr.length-1,temp);
System.out.println("排序之后:");
for(int t:arr){
System.out.print(t+" ");
}
System.out.println();
}
public void merging(int[] arr,int left,int right,int[] temp){
if(left==right){//这个就说明已经不能再拆了
return ;
}
int mid = (right+left)>>1;
merging(arr,left,mid,temp);
merging(arr,mid+1,right,temp);
int i,j;
for(i=left;i<=right;++i){
temp[i] = arr[i];
}
i = left;
j = mid+1;
for(int k=left;k<=right;++k){
if(i==mid+1){
arr[k] = temp[j];
j++;
}else if(j==right+1){
arr[k] = temp[i];
i++;
}else if(temp[i]<temp[j]){
arr[k] = temp[i];
i++;
}else if(temp[i]>temp[j]){
arr[k] = temp[j];
j++;
}
}
}
归并排序的效率:
时间复杂度:O(logn)
空间复杂度:O(n)
快速排序:
快速排序也采用分治策略,但与归并排序不同的是,快速排序先选择一个基准元素,将序列中的元素分割为两部分,一部分大于基准元素,一部分小于基准元素。然后递归地对这两部分进行排序。
选择基准:
从序列中选择一个基准元素。我一般选取的是第一个元素:
int flag = arr[left];//选取一个基准数
分割:
将序列中小于基准的元素放在它的左侧,大于基准的元素放在右侧,基准元素放在中间。
int i = left;
int j = right;
while(i<j){
//j哨兵的任务就是找到第一个比基准数小的数字
while(flag<=arr[j]&&i<j){
j--;
}
//i哨兵的任务就是找到第一个比基准数大的数字
while(flag>=arr[i]&&i<j){
i++;
}
if(i<j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
arr[left] = arr[i];
arr[i] = flag;
递归排序:
对分割后的左右两个子序列进行递归排序。
quickingsort(arr,left,i-1);
quickingsort(arr,i+1,right);
分析:
就拿第一层递归来举例把,当这个flag == arr[0] = 6的时候,
想一下i,j的任务是什么,i是去找第一个大于flag的数,j是去找第一个小于flag的数
ok,我们进入while循环,当arr[i] = 7,arr[j] = 5 时,他们就完成了第一次任务,然后这个时候,就将这两个数进行交换,然后arr数组就会变成:{6,1,2,5,9,3,4,7,10,8}
然后继续做,当arr[i] = 9,arr[j] = 4 时,就又完成了一次任务:进行交换:
arr数组:{6,1,2,5,4,3,9,7,10,8}
然后继续做,arr[i] = 6,arr[j] = 6 的时候,这两个哨兵相遇了,相遇了怎么办呢,说明探测结束,需要将flag和3进行交换,然后arr数组就会变成{3, 1, 2, 5, 4, 6, 9, 7, 10, 8}。
然后就和上面的归并排序就有点像了,就是分别递归再进行排序。
public void quicksort(int[] arr){
System.out.println("排序之前:");
for(int temp:arr){
System.out.print(temp+" ");
}
System.out.println();
quickingsort(arr,0,arr.length-1);
System.out.println("排序之后:");
for(int t:arr){
System.out.print(t+" ");
}
System.out.println();
}
public void quickingsort(int[] arr,int left,int right){
if(left>right){
return;
}
int flag = arr[left];//选取一个基准数
int i = left;
int j = right;
while(i<j){
//j哨兵的任务就是找到第一个比基准数小的数字
while(flag<=arr[j]&&i<j){
j--;
}
//i哨兵的任务就是找到第一个比基准数大的数字
while(flag>=arr[i]&&i<j){
i++;
}
if(i<j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
arr[left] = arr[i];
arr[i] = flag;
System.out.println(Arrays.toString(arr));
quickingsort(arr,left,i-1);
quickingsort(arr,i+1,right);
}
快速排序的效率:
时间复杂度:O(logn)~ O(n2)(最坏的情况就退化成冒泡嘛)
空间复杂度:O(1)
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。
计数排序:
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 [1] 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序) ----------------百度百科
算法过程(步骤):
1:获得这个数组中的最大值,以此来确定额外空间的大小
2:遍历原数组,进行计数,统计每个数字出现的次数
3:根据出现的次数,申请一个新的数组,依次放入即可。
我们拿一个简单的例子[1,1,4,2,1,3]这个数组来进行举例,我们遍历一次,我们知道,最大的数是4.,所以我们申请一个大小为(4+1)的数组就行。
接着我们统计每个数字出现了几次,我们很容易看出来,1出现了3次,2,3,4都各出现了一次。
然后我们申请一个新的数组,通过这个计数的个数,直接得出这个答案数组是1,1,1,2,3,4
具体代码:
class CountSort{
public void CountSort(int[] nums){
//1:获取数组的最大值。
int max = Integer.MIN_VALUE;
int i;
for(i=0;i<nums.length;++i){
if(nums[i]>max){
max = nums[i];
}
}
int[] bucket = new int[max+1];//申请额外空间
//2:统计每个数的出现次数
for(i=0;i<nums.length;++i){
bucket[nums[i]]++;
}
//3:根据每个元素出现的个数排序
int[] ans = new int[nums.length];
int sortindex = 0;
for(i=0;i<max+1;++i){
while (bucket[i]-->0){
ans[sortindex++] = i;
}
}
System.out.println(Arrays.toString(ans));
}
}
这个也可以顺便说一下,就是,这里我用的例子就是[1,1,4,2,1,3]这个数组,数字比较小也比较集中,但是如果给我们的数字数[104,101,103,105]这种数组我们可以用计数排序嘛,答案其实也是可以的。具体怎么做呢?
从上面的代码可以看出,如果数字到100多,我们的数组大小需要开到100多,空间比较浪费,不过我们观察发现,这些数都比较集中,所以我们可以将这个数组开(max-min)的大小。
这样一来相当于我们也就只开了大概五个元素的数组,空间就比较可观。
计数排序的效率和时间空间复杂度:
时间复杂度
O(n+k)。
空间复杂度
O(k)。
稳定性
稳定。
计数排序的缺点:
1.当数列最大最小值差距过大时,并不适用于计数排序
2.当数列元素不是整数时,并不适用于计数排序
计数排序的例题:
代码:
class Solution {
public int heightChecker(int[] heights) {
int[] arr = new int[101];
for (int height : heights) {
arr[height]++;
}
System.out.println(Arrays.toString(arr));
int count = 0;
for (int i = 1, j = 0; i < arr.length; i++) {
while (arr[i]-- > 0) {
if (heights[j++] != i) count++;
}
}
return count;
}
}