冒泡排序
冒泡排序的思想
冒泡排序的主要思想是:相邻两个元素进行比较,较大的元素放在后面。就像水泡一样,慢慢的浮到最后一个。我们通过一个例子,演示一下冒泡排序:
举例说明冒泡排序:
已知一个数组a = {5,9,2,6},如下图所示,我们开始演示一下冒泡排序:
5比9小,不用交换位置
然后比较9和2,9比2大,于是交换9和2的位置,如下图所示:
然后比较9和6,9比6大,于是交换9和6的位置,如下图所示:
此时我们发现,9已经被排到数组的最后,我们称上面的过程为一趟扫描。通过这一趟扫描后,我们可以确定数组中最大的元素。这个元素已经被排在我们数组的最后了。
然后我们进行下一趟扫面, 5比2大,交换位置。如下图所示:
然后比较5和6,5比6小不用交换。注意,由于我们第一趟已经将数组的最大值9放在最后了,我们在以后的几趟中,就无需再比较这些已经被我们排好序的元素了。
代码实现
代码实现:
public static void BubbleSort(int[] a){
for (int i = 0; i <a.length; i++) {
for (int j = 0; j < a.length-i-1; j++) { //-1就是为了重复比较已经排好序的元素。
if (a[j] >a[j+1]){
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
}
}
}
}
快速排序
快速排序的思想
① 在数组中随机找一个枢纽元素。(该枢纽元素用作比较)
② 将小于枢纽元素的放在左半区,将大于枢纽元素的放在右半区。
③ 对左右半区的元素,继续进行②③步。
普通快速排序(双向划分)
举例快速排序:
给定一数组,我们选择数组的第一个元素作为枢纽元素。记作tag。并给定两个指针,分别记作left、right。
通过判断left、right所指向的数据:
如果right所指向的数据大于tag,则right–,直到找到 ar[right] ≤ tag时,使得ar[left] = ar[right]
如果left所指向的数据小于tag,则left++,直到找到ar[left] ≥ tag时,使得ar[right] = ar[left];
当left == right时,我们让这个位置上的元素变为tag。
一次划分的过程:
① 判断right所指向的值 是否 小于 tag。如上图,ar[right] == 30 < tag ==78,所以我们让ar[left] = 30; 如下图:
② 然后判断left所指向的值,是否大于tag,如果不大于,则让left++,直到找到为止。如图,直到left指向89时,我们的array[left]==89 > tag,然后将left所指向的值,赋给array[right]
③ 再次移动right和left。重复①② 过程。
我们再次判断right所指向的值 是否小于tag,如果不小,则一直让right–,直到找到为止。如图,
当right指向45时,array[right] < tag。于是,right所指向的值 赋给 array[left]。
④ 再去判断,left所指向的值,是否大于tag,如果没找到, 则让left++,如图,当left指向100时,array[left]==100 > tag,于是left所指的值,赋给array[right]
⑤ 继续判断 right,right所指向的值是否小于tag,由上图得,array[right] == 100 > tag。所以让right–,此时left指向同一个元素。
这个时候,我们就不用再循环了,将tag赋给array[left]。这个时候,我们就发现,left之前的数据都是小于tag的,left之后的数据,都是大于tag的。
此时,我们将数组分为了左半部分和右半部分,左半部分的数都比tag小,右半部分都比tag大。于是对于左右两个部分,划分过程和上面类似。
一次划分的代码实现:
public static int partition(int[] ar,int left,int right){
int i = left;
int j = right;
int tag = ar[i];
while(i<j){
while(i<j && ar[j] >tag) j--;
if(i<j) {ar[i]= ar[j];}
while(i<j && ar[i] < tag) i++;
if(i<j) {ar[j] = ar[i];}
}
ar[i] = tag;
return i;
}
多次划分的代码实现:
public static void quickSortPass(int []ar,int left,int right){
if(left < right){
int pos = partition(ar,left,right);
quickSortPass(ar,left,pos-1);
quickSortPass(ar,pos+1,right);
}
}
我们能不能将多次划分的代码,不用递归来实现呢? 当然可以,我们可以使用一个队列来实现,代码如下:
private static void niceQuickPass(int[]a,int left,int right){
Queue<Integer> queue = new LinkedList<>();
queue.offer(left);
queue.offer(right);
while(!queue.isEmpty()){
left = queue.poll();
right = queue.poll();
int pos = partition(a,left,right);
if (pos-1 > left){
queue.offer(left);
queue.offer(pos-1);
}
if (pos+1 < right){
queue.offer(pos+1);
queue.offer(right);
}
}
}
快排的改进:随机划分
我们递归实现快排的最好情况下的时间复杂度为: O(nlogn),最坏情况下的时间复杂度为 O(n²)。
快速排序的最坏的情况下是:
① 当数组已经是排好序的。
② 数组中所有的元素都相同。
我们解决这些情况的办法是,让数组进行随机划分。上述我们的快速排序是以始终以数组的最左边的元素进行划分的。
快速排序的随机划分:
public static int randomQuickPartition(int[] a,int left,int right){
Random random = new Random();
int index = random.nextInt((right-left+1)) + left;
int tmp = a[index];
a[index] = a[left];
a[left] = tmp;
return partition(a,left,right);
}
注意上面 我们在计算index时,为什么要再加上left呢?我们举个例子来说明这个问题。
这是我们已经完成了一次划分的数组
对于右半部分来说,如果我们随机得到的数是1,相当于我们右半部分数组的index == 1,如果我们不加left值的话,这个1就不是我们的绝对物理下标。什么意思呢? 就是说,我们随机得到的这个1,是相对于右半部分来说,是在我们右半部分数组中index = 1 的数字,但是对于我整个数组来说,这个数字的绝对物理下标应该加上left。这样才是我们随机产生index的绝对物理下标。
快排的划分:单向划分
上面我们介绍了快速排序的随机划分、双向划分,现在我们来看一看单向划分,什么是单向划分?单向划分就是,我们只从一个方向和枢纽元素进行比较。
具体思路如下:
① 选择一个枢纽元素,我们这里就选择a[left] 为枢纽元素。
② 定义两个指针,分别记作i,j。i指向left,j指向i+1。
③ 判断ar[j] 是否大于tag,若大于,让j++,直到ar[j] ≤ tag,此时i++,并交换i和j位置上的值 。
④ 当 j 超过数组长度时,此时将 a[i] = tag。
图示:
① 定义两指针i,j,分别指向第一个元素和第二个元素。
② 比较a[j] > tah。如上图,a[j] == 90 > tag,于是j++,得到下图:
③ 再比较arr[j] > tag。如上图所示,arr[j] == 88 > tag。让j++,得到下图:
④ 再比较arr[j] > tag。如上图所示,arr[j] == 15 < tag。于是先让i+1,然后交换i和j位置上的值,交换完后如下图所示:
⑤ 再比较arr[j] > tag。如上图所示,arr[j] == 90 >tag。让j++。得到下图:
⑥ 再比较arr[j] > tag。如上图所示,arr[j]==32 <tag,于是先让i+1,再交换i和j位置上的值
⑦ 再比较arr[j]>tag。如上图所示,arr[j] == 88 >tag。于是让j++。得到下图:
⑧ 再比较arr[j]>tag。如上图所示,arr[j] == 23 < tag。于是先让i+1,再交换i和j位置上的值。
⑨ 再比较arr[j] > tag。如上图所示,arr[j] ==90 > tag。于是j++。得下图:
⑩ 再比较arr[j] > tag。如上图所示:arr[j] == 99 > tag,于是j++。得下图:
11.再比较arr[j] > tag。如上图所示,arr[j] == 47 < tag。于是让i+1,交换i和j位置上的值。
再比较arr[j] > tag。如上图所示,arr[j] == 88 > tag。让j++,此时j已经超过了right的范围。此时划分结束。将i位置和left位置上的值进行交换,得下图:
此时我们发现,在i的右边都是比tag小的数,在i的左边都是比tag大的数。此时单向划分一次划分的过程就结束了。
代码实现:
public static int singlePartition(int[] a,int left,int right){
int i = left;
int j = i+1;
int tag = a[i];
while(j <= right){
if(a[j]<=tag){
i++;
change(a,i,j);
}
j++;
}
change(a,i,left);
return i;
}