冒泡排序
实现代码
/**
* @param a:需要排序的数组
* @param n:数组的长度
*/
public void BubbleSort(int[] a, int n){
if (n <= 1) return;
for(int i = 0; i < n; ++i){
// 提前退出冒泡循环的标志位
boolean flag = false;
for(int j = 0; j < n - i - 1; ++j){
if(a[j] > a[j+1]){ // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if(!flag) break; // 没有数据交换,提前退出
}
}
主要注意点
- 为什么循环条件是
for(int i = 0; i < n; i++){
for(int j = 0; j < n-i-1; j++)
}
第一个for循环的作用是:需要冒泡的次数
第二个for循环的作用是:每次冒泡交换和比较的次数
冒泡的原理:冒泡排序可以分为两个部分,一个是冒泡;另一个是冒泡过程中相邻两个元素的对比和交换。其中每一次冒泡,都会将该数组中的最大的数字挑选出来放在数组的末尾。
对于一个长度为n的数组,最多需要进行n次冒泡就可以将该数组从小到大进行排列;每次冒泡,冒泡中的对比和交换不必将已经挑选出来的元素进行对比和交换,所以第二个for循环中的终止条件和冒泡的次数有关系,只需要对比和交换n-i-1次就好
- 通过设置一个flag优化排序算法,当数据没有交换时,证明已经排序完毕。
- 在分析冒泡排序的时间复杂度时,由于不同数组的数据的初始顺序不同,所以排序时冒泡的次数也不同,会影响代码的执行时间,故为了分析时间复杂度的概念,引入了有序度的概念,进行粗略的评估冒泡排序的时间复杂度。
有序度是数组中具有有序关系的元素对的个数。
插入排序
public class InsertSort {
/**
* @param a:需要排序的数组
* @param n:数组的长度
*/
public void insertSort(int[] a, int n){
if(n <= 1) return;
for(int i = 1; i < n; i++){
int value = a[i]; //临时存放a[i],交换时使用
int j = i - 1;
//查找插入位置
for(; j >= 0; j--){
if(a[j] > value){
a[j+1] = a[j];//数据移动,并且需要继续查询
}else{
break;
}
}
a[j+1] = value;//由于循环条件已经将上一轮的j再次减1,所以这里是a[j+1],而不是a[j]
}
}
}
主要注意点
插入排序原理的理解:
-
插入排序可以将数组分为已排序不分和未排序部分
for(int i = 1; i < n; i++){
for(int j = i -1; j >= 0 ; j- -)
}
第一个for循环为遍历数组还未排序的部分
第二个for循环是在数组已排序的部分寻找未排序的数组中的第一个元素的插入位置(通过对比大小的方式,如果已排序的数组中的元素大于待插入的元素,则将该元素向后移动一位,同时继续在已经排好序的数组中寻找位置) -
未排序的数组
需要使用一个变量(value)记录带插入的元素,方便后续插入到已经排好序的数组中(否则随着i的改变以及已排序数组的后移,就会导致该元素的丢失) -
已排序的数组
在已经排序好的数组中寻找元素的插入位置时,需要使用j下标(不使用i下标)的原因有两个:一是考虑到循环对比的情况;而是避免下标混乱。 -
寻找到插入位置后,插入的位置时 a[j+1] 而不是a[j]的原因:循环条件已经将上一轮的j再次减1,上一轮的a[j]其实是现在的 a[j+1]。
选择排序
public class SelectSort {
/**
* @param a:需要排序的数组
* @param n:数组的长度
*/
public void selectSort(int[] a, int n){
for(int i = 0; i < n-1; i++){
int min = i;
//找出数组中未排序的元素中的最小值
for(int j = i+1; j < n; j++){
if(a[j] < a[min]){
min = j;
}
}
if(min != i){
int tmp = a[i];
a[i] = a[min];
a[min] = tmp;
}
}
}
}
主要注意点
- for(int i = 1; i < n-1; i++){
for(int j = i+1; j < n ; j++)
}
第一个for循环中,i<n-1(只需要到倒数第二个数据即可)的原因:倒数第二个数据和最后一个数据进行比较后,就可以判断最后一个数据的大小。 - 选择排序是不是稳定的排序算法,如5,8,5,2,9这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素2,与第一个5交换位置,那第一个5和中间的5顺序就变了,所以就不稳定了。
归并排序
/**
* @param a:需要排序的数组
* @param n:数组的长度
*/
public void mergeSort(int[] a, int n){
merge_sort(a,0,n-1);
}
//归并函数(通过递归实现)
public void merge_sort(int[] a,int p, int r){
//终止条件
if(p >= r) return;
//数组的中间值
int mid = (p+r)/2;
//分治递归
merge_sort(a,p,mid);
merge_sort(a,mid+1,r);
//数组合并,将前面的数组和后面的数组合并
merge(a,p,mid,r);
}
public void merge(int[] a, int p, int mid, int r){
int[] tmp = new int[r-p+1];
int i = p;//指针:指向前一个数组的第一个元素
int j = mid + 1;//指针:指向后一个数组的第一个元素
int m = mid;//数组的中间元素
int k = 0;//临时数组的下标
while(i <= mid && j <= r){
if(a[i] < a[j]){
tmp[k++] = a[i++];
}else{
tmp[k++] = a[j++];
}
}
while(i <= mid){
tmp[k++] = a[i++];
}
while(j <= r){
tmp[k++] = a[j++];
}
for(i=p,j=0; i<=r; i++,j++){
a[i] = tmp[j];
}
}
}
主要注意点
- 归并排序主要分为两步:第一步是将一个数组的排序问题分解为两个子数组的排序问题,直到满足终止条件(每个子问题中只包含一个数据);第二步将子数组合并,并在合并的过程中排序。
- 递归函数的确定
递归函数的入参包括数组、数组/子数组首位数据的下标p和末位数据下标r
关键是写出递归公式,明确递归的终止条件 - 合并函数
合并函数中有几部分元素:a.合并(排序)时的新数组;b.用于合并(排序)的指针;c.合并(排序)的实现方法。
合并(排序)时的新数组的大小一定要设置成动态,并且不要忘记+1(n = r-p+1)
用户合并的指针:分别指向前一半子数组的首位,后一半子数组的首位和新数组空间首位
合并(排序)的实现方法:简化写法tmp[k++] = a[i++]先取变量的值,然后再给变量增加1;注意分情况讨论的,分别为前一半子数组和后一半子数组中均有元素等待排序、前半子数组中的元素已经用完和后半段子数组的元素已经用完三种情况。
归并排序
public class QuickSort {
public void quickSort(int[] a, int n){
quick_sort(a,0, n-1);
}
public void quick_sort(int[] a, int p, int r){
if(p >= r) return;
int pivot = partition(a,p,r);//获取分区点,大于分区点元素的在分区点右侧,小于分区点的元素在分区点左侧
quick_sort(a,p,pivot-1);
quick_sort(a,pivot+1,r);
}
public int partition(int[] a, int p, int r){
int i = p;//记录第一个大于pivot元素的位置(方便出现小于pivot的元素是定位位置),如果后面j遍历的元素小于pivot,则该元素和记录i的位置进行交换
int pivot = a[r];
for(int j = p; j < r; j++){
if(a[j] < pivot){
int tmp = a[j];
a[j] = a[i];
a[i] = tmp;
i++;
}
}
a[r] = a[i];
a[i] = pivot;
return i;
}
}
主要注意点
-
快速排序的关键点有两个:一是递归思想,注意明确递归的终止条件和递归公式;二是分区点选取函数的实现。
-
分区点选取函数的实现
分区点选取函数有两个任务,一是返回分区点的位置下标,二是将数组中的数据变为分区点左边小于分区点数据,分区右边大于分区点数据的结构,在此过程中涉及到i,j两个位置数据的交换(如果需要)。
初始化两个指针i,j,其中i指针记录记录第一个大于pivot元素的位置(小于pivot数据的尾部);j用来遍历整个数组的数据。
分区元素一般选数组中的最后一个数据,并在对比结束之后将其位置和i所记录的位置交换。返回i的位置下标。 -
如果对于已经排好序的数组,如1,2,3,4,5,6,7
第一轮递归返回的分区点的数值为6,quick_sort(a,pivot+1,r)中的pivot+1=7(数组最大为a[6]),但是由于递归结束条件的限制(p >= r),不会出现异常。
计数排序
/**
* @param a:需要排序的数组
* @param n:数组的长度
*/
public void countingSort(int[] a, int n){
if(n <= 1) return;
// 需要确定需要排序的数组中,最大值和最小值的范围
int max = a[0];
for(int i = 0; i < n; i++){
if(a[i] > max){
max = a[i];
}
}
// 为什么申请的数组长度为max+1?
// 答:因为是从0开始计数,所以需要max个元素进行储存。
int[] c = new int[max+1];
// 为什么要给数组中的元素全都赋值为0?数组初始化不应该就是0吗?
for(int i = 0; i <= max; i++){
c[i] = 0;
}
// 计算每个元素的个数,并且写入新数组
// 操作数组:a[]
for(int i = 0; i < n; i++){
c[a[i]]++;
}
// 新数组顺序求和,获得最终数组
// 操作数组:c[]
for(int i = 1; i <= max; i++ ){
c[i] =c[i-1] + c[i];
}
// 临时数组,储存排序结果
int[] tmp = new int[n];
// 计算排序
for(int i = n-1; i >= 0; i--){
int index = c[a[i]]-1;//a[i]中的数据;c[a[i]]小于等于该数据的数据个数; c[a[i]]-1:该数据排序后的位置(-1是因为数组是从0开始计数)
tmp[index] = a[i];
c[a[i]]--;
}
// 将结果复制至a[]
for(int i = 0; i < n; i++){
a[i] = tmp[i];
}
}
}
问题:
1.计数排序为什么要从后向前遍历需要排序的数组为了保证排序的稳定性
主要注意点
- 计数排序主要步骤
a.在待排序的数组中找到最大数据,确定数据范围
b.创建计数数组(数组的下标为数据,内容是相同的数据出现的次数)
c.将计数数组进行顺序加和(数组下标还是数据,内容是小于等于该数据的数据个数)
d.(核心)创建临时数组,进行数据排序
通过c中的计数数组,确定排序数组中的数据在排好序时的下标