冒泡排序
冒泡排序顾名思义就是像水中的泡泡一样,离水面越近,泡泡就会越大。
在数组中的体现就是从数组的一号索引开始逐渐像后比较,遇到比当前值大的数就进行交换,利用两层循环实现全数组的有序。
代码分为三部分:
1.外层循环,每一次循环保证最大的数排在他对应的位置,因为最后一个数不用排所以次数-1
2.内层循环,对已经排好序的数之外的数进行排序,两个相邻数互换位置,直到排好当前最大数的位置为止
3.交换算法,使用位运算的方式交换位置可以提高效率
代码
public int[] bubbleSort(int[] arr){
for(int i = 0 ;i < arr.length-1;i++){ //最后一个排所以只需要走数组长度-1次
for(int j = 0;j <= arr.length-i-1;j++){
//用内层循环来控制两个数的交换和比较
if(arr[j] > arr[j+1]){
swap(arr,i,j);
}
}
}
return arr;
}
public static void swap(int[] arr,int a,int b){ //将数组中两个数互换位置的方法
//位运算符号更高效,但局限性就是arr[a]和arr[b]本质上不能是同一个数
arr[a]=arr[a]^arr[b];
arr[b]=arr[a]^arr[b];
arr[a]=arr[a]^arr[b];
}
下面来详细介绍一下交换方法swap
我们都知道a^0=a,a^a=0,所以我们可以利用位运算的特点完成交换
首先令a=a^b
接着令b=a^b,现在的b=a^b^b=a
最后令a=a^b=a^b^a=b完成了交换操作
注意,这个方法只适用于a和b本质上不相同的情况,如果出现相同情况交换会失败
时间复杂度
冒泡排序的时间复杂度主要取决于两个嵌套循环的迭代次数。外层循环的迭代次数为 (n-1) 次(其中 (n) 是数组的长度)。对于每次外层循环迭代,内层循环的迭代次数会减少一次(因为每次迭代都会将一个元素放到正确的位置)。
在最坏情况下(数组是逆序的),每次内层循环需要比较 (n-i-1) 次,其中 (i) 是外层循环的索引。总的比较次数是:
[
(n-1) + (n-2) + (n-3) + \cdots + 1 = \frac{(n-1) \cdot n}{2}
]
因此,时间复杂度是 (O(n^2))。
在最好的情况下(数组已经是排序好的),冒泡排序仍然需要进行 (n-1) 次外层循环和内层循环中的部分比较,但可以通过优化(如添加标志位来检测是否发生了交换)来减少比较次数。但在没有优化的情况下,时间复杂度仍然是 (O(n^2))。
空间复杂度
冒泡排序是一种就地排序算法,不需要额外的存储空间来存放临时数据。只需要几个额外的变量来控制循环和交换元素,因此空间复杂度是 (O(1))。
快速排序
快速排序算法是对冒泡排序的一种改进算法,平均时间复杂度为(O(nlogn)),比冒泡排序提高很多
快排的主要思想是在数组中随机挑选一个数作为分界点,将数组分为三个部分,小于区,等于区,大于区,接着递归实现各区的有序从而达到整体的有序
代码
public void quickSort(int[] arr,int l,int r){
//参数说明,给定数组,数组的最小索引,数组的最大索引
int n = l + (int)(Math.random() * (r-l+1));
//在数组中随机位置选取一个索引,使用random函数有助于平均时间复杂度
swap(arr,n,r); //将选中位置的数与数组最右边的数进行交换作为标准
int[] p=partition(arr,l,r); //返回等于区的左边界和右边界
//左区递归,右区递归
quickSort(arr,l,p[0]-1); //pratition得到的左区边界成为左区的右边界递归去排序
quickSort(arr,p[1]+1,r); //pratition得到的右区边界成为右区的左边界递归去排序
}
public static int[] partition(int[] arr,int l,int r){//l作为指针控制一个个数据交换
int less=l-1; //作为左区小于部分的界限
int more=r; //作为右区大于部分的界限
while(l < more){
if(arr[l] < arr[r]){
swap(arr,++less,l++); //左区外扩指针右移,将左区后一个数和当前数做交换
}else if(arr[l] > arr[r]){
swap(arr,--more,l);
//右区左扩,将右区前一个数据与当前数做交换,指针不变,因为新换过来的数据还没有比较过
}else{
l++;//这是相等的情况,只需要指针右移就好了
}
}
swap(arr,more,r); //最后将最右侧的标准和右区内第一个数交换就做到了有序
return new int[]{less+1,more};
}
public static void swap(int[] arr,int a,int b){
int t=arr[a];
arr[a]=arr[b];
arr[b]=t;
}
时间复杂度
随机选择基准元素是为了在平均情况下提高性能。使用 int n = l + (int)(Math.random() * (r - l + 1));
从当前范围 [l, r]
中随机选择一个元素,将其与数组的最右边的元素交换。这有助于避免最坏情况,并通常使时间复杂度接近 O(nlogn)O(nlogn)。
在 partition
函数中,使用双指针方法对数组进行分区。这个操作的时间复杂度是 O(n),因为每个元素最多被处理一次。
快排的递归调用将问题规模减小。最优情况下,每次划分都能平分数组,递归深度为 logn,每层的分区操作是 O(n)。因此,平均情况下时间复杂度是 O(nlogn)
最坏情况下,快排可能退化成 O(n^2),例如,当每次划分的基准元素是数组的最大或最小元素时,递归深度达到 n,这使得时间复杂度达到 O(n^2)。
空间复杂度
空间复杂度的主要部分是递归调用栈的空间。在最优情况下,递归深度为 logn,因此空间复杂度为 O(logn)。在最坏情况下,递归深度为 n,这会使空间复杂度达到 O(n)。
归并排序
将整个数组分为两部分,保证每个部分都是有序的最后每个部分之间在排序做到整体的有序
代码:
public void MergeSort(int[] arr,int l,int r){
if(l==r){
return;
}
int mid=l+((r-l)>>1); //利用位运算计算中间数提高效率
MergeSort(arr,l,mid); //左边递归
MergeSort(arr,mid+1,r); //右边递归
merge(arr,l,mid,r);
}
public void merge(int[] arr,int l,int mid,int r){
int help[]=new int[r-l+1]; //申请一个额外空间用来存放有序的数组
int i=0; //指针
int p1=l; //小于部分指针
int p2=mid+1; //大于部分指针
while (p1<=mid && p2<=r){
help[i++]=arr[p1]<arr[p2] ? arr[p1++] : arr[p2++];
}
//一定会有p1或者p2后续的节点没添加完但其中一个到头了,那么就用剩下的补齐help数组
while (p1<=mid){
help[i++]=arr[p1++];
}
while (p2<=r){
help[i++]=arr[p2++];
}
//将排好的数组赋值给arr
for (int k = 0; k < help.length; k++) {
arr[l+k]=help[k];
}
}
时间复杂度
1.分解阶段:MergeSort 将数组分成两半,递归地对每半进行排序,递归深度为 log(n),其中 n 是数组长度。
2.合并阶段:merge 方法在每一层递归中合并两个有序子数组的时间复杂度是 O(n)。
总的时间复杂度是 O(n log n),因为每层递归的合并操作需要 O(n) 时间,递归深度为 log n。
空间复杂度
递归栈空间:递归深度为 log n,每层递归占用常量空间。
额外空间:merge 方法中使用了一个大小为 n 的辅助数组 help。
总的空间复杂度是 O(n),主要是由于辅助数组 help 的使用。