1.冒泡排序
排序原理不在多说,直接上例子,通过例子能够一点点的认识到冒泡排序的原理及过程,大家看懂后可以自己总结各个排序的原理发到评论区,如有写错的地方,希望读者们不吝指正。另外,各个小节中的举例都以升序排序;
无序数组(长度为6):9 2 0 1 5 7
第一趟冒泡:将最大的数字冒到数组末尾
按照图中:从左至右两两比较,发现左边大于右边就交换两数的位置的方式,一直重复直至比较(遍历)完全部元素,我们便完成了第一趟排序,成功的将数组中最大的数字放到了数组末尾:
第一趟共比较了5次,发生了5次交换
第二趟排序:对剩余的左边五个元素进行冒泡,将第二大的数字冒到倒数第二个位置:
第二趟排序将第二大的数字7冒到了9的左边, 此时数组中元素变为:
第二趟共比较了4次,发生了2次交换
第三趟排序:对剩余的左边四个元素进行冒泡,将第三大的数字冒到倒数第三个位置:
第三趟排序将第三大的数字5冒到了7的左边, 此时数组中元素变为:
第三趟共比较了3次,发生了0次交换
第四趟排序:对剩余的左边三个元素进行冒泡,将第四大的数字冒到倒数第四个位置:
第四趟排序将第四大的数字5冒到了5的左边, 此时数组中元素变为:
第四趟共比较了2次,发生了0次交换
第五趟排序:对剩余的左边两个个元素进行冒泡,将第五大的数字冒到倒数第五个位置:
第五趟共比较了1次,发生了0次交换
到这里我们一共进行了5次冒泡,将最大的五个泡泡冒到了右边,此时还剩下一个泡泡:0,但数组中一共6个泡泡,已经将最大的5个放到了最后,那么剩下的1个一定是最小的,所以到这里我们就完成了数组的排序。
编码思路:
在上面我们已经看完了冒泡排序算法排序的全过程,我们发现数组中一共 6(n)个数字,我们需要进行 5(n-1)趟冒泡,另外我们还发现,第 1 趟冒泡的比较次数为5(n-1)次,第 2 趟比较次数为4(n- 2 )次,第 3 趟比较次数为4(n- 3 )次..........
以此类推我们可以知道:第 k 趟比较次数为(n- k )次
结论:
- 待排序数组大小为 n, 则需要 n-1 趟冒泡,其中第 k 趟冒泡的比较次数 n-k 次。
- 那么总的比较次数为(交换次数这里不讨论):
- 所以我们的冒泡排序时间复杂度为
代码:
public static void bubbleSort(int[] array){
for(int i = array.length-1; i > 0; i--){//外循环控制冒泡的趟数
for(int j = 0; j < i; j++){//内循环控制比较次数
if( array[j] > array[j+1] ){//比较过程中,发现左边大于右边则交换
swap(array,j,j+1);
}
}
}
}
//交换元素的方法
public static void swap(int[] array,int i,int j){
array[i]=array[i]^array[j];
array[j]=array[i]^array[j];
array[i]=array[i]^array[j];
}
看到这里,恭喜你已经学会冒泡排序了,但还有个好消息是冒泡排序还可以改进一下,现在我们回头看一看上面的冒泡排序过程,你会发现其实我们完成第二趟排序之后,数组中的元素已经有序了:
所以我们后面的3趟冒泡完全是在做无用功,所以,我们是否可以程序中加上一个判断条件,在每一趟排序后,来检验我们数组是否已经有序了,以避免上面那种做无用功的情况,答案当然是yes,怎么加判断条件呢?每一趟冒泡后遍历检查一下前面的元素是否有序?这显然不行,为啥不行,小伙伴们可以自己思考一下,有思路的可以分享到评论区共勉,下面我说一下我的思路:我们回头去看一下,当数组有序后,剩下的几趟冒泡中有什么共同特点:交换次数都为 0 !!!不难理解,我们是希望将大小顺序不符合预期的数字进行交换,既然数组已经有序了,那么相邻两个元素必然是左小右大,所以不会发生交换,按照这个理论,我们便可以对程序进行优化,话不多说,怎么优化,直接看代码。
public static void bubbleSort1(int[] array){
for(int i = array.length-1; i > 0; i--){
boolean swaped=false; //加一个判断,默认为此次遍历未发生交换
for(int j = 0; j < i; j++){
if( array[j] > array[j+1] ){
swap(array,j,j+1);
swaped=true; //一旦发生了交换,则将其改为true
}
}
if(!swaped) break; //当某次遍历结束后,未发生交换,说明数组已经有序,则直接退出
}
}
//swap()与上相同
但实际上这种优化的意义不大,因为我们遇到的数组中,有的数组(如:9 8 7 6 5 4 3 2 1)是必须要进行N趟冒泡才能使得整个数组有序的,遇到这样的数组,我们不但不能提前结束循环,还得在每一趟冒泡后进行一次判断以及对 swaped 赋值,增加了工作量,另外还浪费了更多的空间(一个字节也算浪费)
2.选择排序
还是直接先上例子,算法原理和过程留给大家总结:
待排序数组:
第一次遍历:找出数组中最大的元素: 9,9 对应下标为 0 ,0 不等于 3,说明最大的元素没有放在数组末尾,所以交换 0 和 3 中的元素:
第二次遍历:找出数组中剩余的3个数中最大的元素: 2,2 对应下标为 1 ,1 不等于 2,说明最大的元素没有放在数组末尾,所以交换 1 和 2 中的元素:
第三次遍历:找出数组中剩余的2个数中最大的元素: 1,1 对应下标为 0 ,0 不等于 1,说明最大的元素没有放在数组末尾,所以交换 0 和 1 中的元素:
三次选择后,我们前三大的元素都放在了相应位置,剩下最后一个元素,必然是有序的,所以长度为4的数组,我们一共要选择3次。
编码思路:
我们回看上面的例子,不难发现,选择排序实际上就是两个步骤:
- 找出剩余元素中最大的数的下标
- 如果找到的下标不是剩余元素中的最大下标,则交换元素
结论:
- 长度为n的数组,需要找出前(n-1)大的元素,并将找到的元素放到剩余元素的最右边
- 寻找 第 k 大的数时,需要比较 n-k次
- 每次找到最大数下标后,交换次数最大为1次
总的比较次数为(总的交换次数最大为 n-1 次):
代码:
public static void selectionSort(int[] array){
for(int i=0;i<array.length-1;i++){
int minIndex=i;
for(int j=i+1;j< array.length;j++){
minIndex = array[minIndex] <= array[j] ? minIndex : j;
}
if (minIndex != i) swap(array,minIndex,i);
}
}
public static void swap(int[] array,int i,int j){
array[i]=array[i]^array[j];
array[j]=array[i]^array[j];
array[i]=array[i]^array[j];
}
3.插入排序
插入排序可以想象为打牌,我们在摸牌的过程中,每摸起一张牌时都会主动的将其放在一个中间位置(中间位置:比如你手里有两张牌:J 和 K,而你摸到的牌为Q,那你会主动将Q这张牌放在J和K之间,这个位置就是中间位置),另外当我们摸到第一张牌时,是不需要进行中间位置插入的,所以插入排序,一般都以数组中第二个元素开始进行插入。
待排序数组:5 2 1 3 4
插排图示:
编码思路:
从数组中的第2个元素开始,将该元素作为待插入元素,依次向左比较,找到中间位置后进行插入,每执行一次插入后,选取下一个待插入元素以相同方式进行插入,直到完成最后一个元素的插入,整个数组就变得有序了。
结论:
- 待排序数组长度为 n ,则需要进行 n-1 次插入
- 第 k 次插入的比较次数最大为:k
- 所以最坏的情况下,总的比较次数为
- 所以我们的冒泡排序时间复杂度为
另外比较次数还有一个下限为 n-1,为什么?这里留给大家作为一个小作业,有思路的可以放在评论区。
代码:
public static void insertSort(int[] array){
for(int i=1;i<array.length;i++){//拿取第i张牌
for(int j=i; j > 0;j--){//通过交换的方式进行插入
if(array[j] >= array[j-1]) break;
swap(array,j,j-1);
}
}
}
public static void swap(int[] array,int i,int j){
array[i]=array[i]^array[j];
array[j]=array[i]^array[j];
array[i]=array[i]^array[j];
}