一、插入排序(我们按升序进行,下边的所有排序也是按升序进行的)
1.基本步骤:
-
给定一个数组,我们为了方便插入要排序的元素用bound来作为边界。将[0,bound)设定为已排序区间,将[bound,arr.length)设定为待排序区间。
-
将bound的值设定为1,想一下,要是只有一个元素,那么它一定是有序的,所以从bound后边的元素开始和bound前边的元素进行比较,要是比找到了比arr[bound]大的元素,就将这个元素向后搬运,
要是小于或者等于的时候就找到了要插入的位置,为cur+ 1位置取等号的原因是为了稳定性
。 -
最后将bound位置的元素,给bound + 1 位置进行插入即可
2.代码实现(内部有详细注释):
public class insertSort {
// 1.插入排序法的练习:(以升序为例)
// 基本思想:设定已排序区间和待排序区间,然后r让带排序区间的第一个值和已排序区间的最后一个值
// 以及最后一个值的前边的所有的数字进行比较,直到找见那个小于带排序区间的第一个值的位置位置,插入即可
public static void insertSort(int[] arr) {
// 设定区间:
// 排以序区间:[0,bound);
// 待排序区间:[bound,arr.length);
int bound = 1;// 不用从0开始,因为带排序区间是前闭后开的,所以这个一个元素,肯定是有序的
// 外层循环,来让bound每一次向后走,直到到达数组的长度为止
for (; bound < arr.length; bound++) {
// 这里用一个变量将bound位置的值记录下来,方便后边进行比较,赋值
int temp = arr[bound];
int cur = bound - 1;
// 里层循环来比较和以排序区间得值的大小
for (; cur >= 0; cur--) {
if (arr[cur] > temp) {//带了等号就不稳定了
// 这里不是交换,是进行元素的搬运哦
arr[cur + 1] = arr[cur];
} else {// 不用交换,这里直接跳出,可以用break,原因是,要找到一个不满足条件的
// 前边一定不满足条件,必定,我们已排序区间是排好序的
break;
}
}
// 等进行完了这次循环后,就找到到了那个比bound小的那个位置,而那个位置就是cur
// 所有bound想被插入的位置就是cur + 1 位置。
arr[cur + 1] = temp;// 上边我们将bound记录下来,就是为了,这里的好插入
}
}
3.注意细节:
-
bound值的给定是从1开始的,因为只要给定一个数字,那么它一定是有序的,
-
牵扯两层循环,外层循环负责依次给数组中的所有元素进行排序,内层循环是和前边的以排序的内容比较进行具体的排序
-
在内部循环中的条件判定
arr[cur] > temp,不能带等号,为了稳定性,带上等号之后,要是遇见相同的数字,就会改变原来两个相同元素的位置
-
在找到要比temp小的元素的时候,实际要插入的位置是cur的后边的元素,也就是cur + 1位置的元素。
4.性能分析
-
时间复杂度:平均O(N * 2)
-
空间复杂度:O(1)
-
稳定性:稳定
-
特点:数组较短的时候和数组相对有序的时候效率高
二、希尔排序
1.基本步骤:
- 先给数组进行分组,gap表示的是组数
- 在分组不小于一的时候循环进行插入排序即可
2.代码实现(内部有详细注释):
// 2.希尔排序(以升序为例)
// 基本思想:将前边的插入排序进行了优化,对原来的数组进行了分组,还是设置了已排序区间和待排序区间
public static void shellSort(int[] arr) {
// 在分组的时候用的是希尔序列 gap = length/2,每一次给gap除以2
int gap = arr.length / 2;
// 这里有个进入函数的条件
while (gap >= 1) {// 就是说当gap 的分组是一组的时候就不用再进行函数的进行了
_shellSort(arr, gap);
// 每进行一次分组,给gap的值除以2.
gap = gap / 2;
}
}
public static void _shellSort(int[] arr,int gap) {
int bound = gap;
for (; bound < arr.length; bound++) {
// 记录bound的值,方便后边进行赋值操作
int temp = arr[bound];
int cur = bound - gap;
for (; cur >= 0; cur -= gap) {
// 进行比较,要是bound下标的值比cur下标的值小,则进行元素的搬运
if (arr[cur] > temp) {
arr[cur + gap] = arr[cur];
} else {// 直接跳出
break;
}
}
// 一轮循环结束之后将temp的值插入到合适的位置
arr[cur + gap] = temp;
}
}
3.注意细节:
- 在选取gap的时候,我们选取的是希尔序列,gap的初始值是数组长度的一半,往后一直折半。
- gap的意思是组数,也是每一组相邻元素之间的下标之差
- 在进行cur的值加减的时候,我们加减的是gap
- 插入位置是 cur + gap 位置
- 我们可以发现当gap为1的时候,与插入排序完全相同
4.性能分析:
- 时间复杂度:平均O(n * 1.3)
- 空间复杂度:O(1)
- 稳定性:不稳定
三、选择排序法
1.基本步骤
- 就好像是打擂台,选取bound为0位置作为擂主
- 两层循环,外层循环负责来移动
每一轮
的擂主,内层循环用来让擂主后边的元素跟擂主较量 - 从bound后的
每一个元素
开始根bound擂主进行比较,要是比擂主小,则进行交换,要是大于等于则进行下一个元素的较量。
2.代码实现(内部有详细注释)
public class selectSort {
// 1.选择排序法(按照升序的方式进行的)
// 基本思想:还是先设置已排序区间和待排序区间,让bound从0 开始进行比较
// [0,bound)表示已排序区间,[bound,length)表示待排序区间
public static void selectSort(int[] arr) {
int bound = 0;// 让bound从数组零号下标开始找
for (; bound < arr.length; bound++) {
int cur = bound + 1;// 上边的每一个bound表示的是擂主,所以后边要进行每一个元素和擂主之间的较量
for (; cur < arr.length; cur++) {
// 里边进行和擂主的比较,要是后边的元素比bound大就要进行交换,
// 要是没有原来的大,就跳出内层循环,进行下一次的比较
if (arr[bound] > arr[cur]) {// 擂主打不过cur,cur成为新的擂主
swap(arr,bound,cur);
} else {
continue;
}
// 注意后边不能加else,跳出循环,要是加上的话,就会跳出这个循环
// 也就是说吧后边的cur还没有比较完,就结束了,肯定是错的
// 返回也是可以的,但是要用的是continue,break不行
// continue 值的是跳出本轮的循环,也就说只是跳过了一次cur,下一次循环还是会进行的
// 要是break的话,就会直接跳出本轮的循环,导致下一个cur不能进行比较了
}
}
}
public static void swap(int[] arr,int x,int y) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
3.注意细节:
- 在进行和擂主比较的时候不能取等号,取了就不稳定了
- 擂主的位置一定是从0号下标开始的,必定我们不知道0号位置的下标的元素是否是最小的。
- 它和冒泡排序法有点像,区别就是,选择排序是一个人和一轮人比较,冒泡是相邻的两个之间的比较
4.性能分析:
- 时间复杂度:O(n * 2)
- 空间复杂度:O(1)
四、堆排序(重点)
基本步骤
- 先针对数组进行建堆操作
- 循环交换堆顶元素和堆的最后一个元素,并将数组的长度减减,此时的堆已经无序了。
- 从0号位置开始向下调整
代码实现(内部有详细注释)
public class heapSort {
// 题目要求:堆排序来进行对一组数据的升序排序
public static void heapSort(int[] arr) {
// 1.要想进行堆排序先要进行建堆
createHeap(arr);
// 2.交换堆顶元素和最后一个元素,并进行对每个元素进行向下调整
int heapSize = arr.length;
for (int i = 0; i < arr.length; i++) {
swap(arr,heapSize - 1,0);// 交换两个元素
// 每交换完一次,先进行堆的长度-1操作
heapSize--;
// 交换完成之后,堆的样子就会变化的,在从0号位置进行向下调整
shiftDown(arr,heapSize - 1,0);
}
}
// 3.建堆代码
public static void createHeap(int[] arr) {
// 在进行建堆的时候,要进行想下调整,只需要从最后一个孩子的上一个父亲节点开始即可
for (int i = (arr.length - 1 -1) / 2; i >= 0; i--) {
// 对每一个元素进行向下调整
shiftDown(arr,arr.length,i);
}
}
// 4.向下调整代码
public static void shiftDown(int[] arr,int size,int index) {
// 设定孩子的起始位置
int parent = index;
int child = 2 * parent + 1;// 父亲与孩子的关系
while (child < size) {// 孩子的下标小于数组的长度的话就进行调整
// 调整左右孩子,将较大值定义为child
if (child + 1 < size && arr[child + 1] > arr[child]) {
child = child + 1;
}
// 比较父亲和孩子的大小
if (arr[parent] < arr[child]) {
// 交换父亲和孩子的位置
swap (arr,parent,child);
} else {// 父亲比孩子大,直接跳过
break;
}
// 更新孩子的位置
parent = child;
child = 2 * parent + 1;
}
}
// 5.交换的方法
public static void swap(int[] arr,int x,int y) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
}
注意细节
- 建堆的时候,
从倒数第一个非叶子节点开始即可,就是(arr.length - 1 - 1) / 2 开始的
。 - (arr.length - 1 - 1) / 2含义:arr.length - 1表示的数组最后一个元素的小标位置,将它看成是一个整体再减一除以二,表示的是父亲和孩子的位置关系
建堆
的时候是从后给前遍历数组的下标进行向下调整
的- 在交换堆的第一个和最后一个元素后,要先长度减减,在进行向下调整,不然下标会乱套
- 在向下调整的过程中不要忘了每一父亲和孩子的更新
性能分析
- 时间复杂度:O(n * log(n))
- 空间复杂度:O(1)
五、冒泡排序法
基本步骤
- 弄上两个循环,外层循环用来表示一轮冒泡,内层循环用来排列每一个数字
- 在每一轮里比较相邻的两个元素,要是前者比后者大则进行交换
- 重复上边的步骤,直到到达数组的最后一个元素即可
代码实现(内部有详细注释)
public class bubbleSort {
// 冒泡排序:(以升序为例)
// 基本思想:设定一个bound,[0,bound)是待排序区间,[bound,size)是已排序区间
// 每进行完一次就将一个元素放在了最后边
public static void bubbleSort(int[] arr) {
for (int bound = 0; bound < arr.length; bound++) {
// 里边进行每两个元素之间的比较,
// for 循环里边要注意:每一次bound的值都会变化的,不要忘记给里边的cur的值减去bound
// 还有一个要注意的是:上边有个for 循环,走到这里说明已经进入循环了,所以此时的bound
// 已经变成了1了,后边在设定cur值的时候,不能是bound,不然不能排序第一个元素
for (int cur = 0; cur < arr.length - 1 - bound; cur++) {
if (arr[cur] > arr[cur + 1]) {// 交换两个值的内容
swap (arr,cur,cur + 1);
}
}
}
}
public static void swap(int[] arr,int x,int y) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
注意细节
- 还是要有两个循环,外层用来控制每一轮的比较,内层循环用来控制每一个数字的冒泡位置
- 内层循环中的cur的设定,因为上边的bound一直在变换,所以后边的cur总是要在数组的基础上减去bound,并且后边要进行给arr[cur + 1]解引用,所以给cur又要减去1
性能分析
- 时间复杂度:O(n * 2)
- 空间负责度:O(1)
六、快速排序法(重点,难点)
基本步骤
- 选取一个基准值,可以选取左边也可以选取右边。我们选取最后边
- 从左给右找出第一个比基准值大的数字
- 从右给左找出第一个比基准值小的数字
- 交换两个值,再交换重合位置和原来基准值的位置
- 返回出上边重合的位置,将原来的数组分成了两个部分,再递归处理这两个部分,直到只有一个元素的时候返回
代码实现
public class quickSort {
// 快速排序法
// 基本思想:找一个基准值,然后弄两个用引用来控制左边和右边的下标
public static void quickSort(int[] arr) {
// 用一个_quickSort方法来辅助进行快排,left 和 right 是闭区间
_quickSort(arr,0,arr.length - 1);
}
public static void _quickSort(int[] arr,int left,int right) {
while (left >= right) {
// 里边要不是空区间要不只有一个元素直接返回即可
return;
}
// 否则进行从左给右找元素和从右给左找元素,这里要找到那个分割线的下标
int index = partition(arr,left,right);
// 递归处理左区间
_quickSort(arr,left,index - 1);
// 递归处理右边的区间
_quickSort(arr,index + 1,right);
}
// 这个方法就是为了交换左边和右边的元素,获取重合位置的下标
public static int partition (int[] arr,int left,int right) {
// 先将这个基准值记录下来
int v = arr[right];
int l = left;
int r = right;
// 这里的条件不能加等号,要是相等的时候就说明已经遍历完成了
while (l < r) {
// 从左给右找第一个比基准值大的元素
while (l < r && arr[l] <= v) {// 因为是想找大于基准值的,所以这里要是arr[l] < 基准值
// 就跳过,当出这个循环的时候,就说明一种情况是:l和r重合了,
// 一种情况就是找到了那个第一个大于基准的数,还有要注意的是:要带上等号
l++;
}
// 从右给左找第一个比基准值小的数字
while (l < r && arr[r] >= v) {
r--;
}
// 经过上边的两个循环之后,就找到了这两个值了,下边进行交换
swap (arr,l,r);
}
// 这里走完后就说明找到了那个重合的位置了,交换基准值和重合位置下标的值
swap (arr,l,right);
// 最后将这个重合的下标返回出去
return r;
}
注意细节
- 在进行递归的时候,我们的区间采用的是前闭后闭的,方便后边进行左右区间的划分。
- 在每一个元素和基准值比较的时候要是遍历的值和基准值相等的情况下也是要跳过的,还是为了稳定性的保持
- 每进行一次外层的循环要交换重合值和基准值
性能分析
- 时间复杂度:平均O (n * log(n))
- 空间复杂度:平均O(log(n))
快排的优化点
- 三数取中
- 在快排的过程中当待处理的区间较小的时候,我们直接对该区间进行插入排序
- 要是递归的深度较大,但是待处理的区间还是较大的时候,我们采用堆排的方法来进行
七、归并排序法(重点,难点)
基本思想:
先将要排序的数组变化成两个有序的数组,然后将两个有序的数组进行拼接即可
基本步骤
- 先将数组无限的划分,直到两个数组的长度都是1的时候,这时的两个数组肯定是有序的,必定只有一个数字的数组是有序的
- 在进行数组无限分割,直到一个元素的时候要用到递归,用mid来记录数组的中间位置,然后不停的划分
- 下来就是合并有序数组,开辟一个新的数组,新的数组的长度和原来的数组的长度一样
- 给有序的两个数组下标编号,依次比较两个数组的首元素的大小,将小的插入到创建好的新的数组里边
- 最后将那个排好序的数组的下拷贝到原来的数组中去
代码实现(内部有详细注释)
public class mergeSort {
// 归并排序
public static void mergeSort(int[] arr) {
_mergeSort(arr,0,arr.length);// 区间让前闭后开
}
public static void _mergeSort(int[] arr,int left,int right) {
// 返回的条件是左右下标的数字相差一个以下,也就是另个数字之间没有区间或者相挨着的时候
if (right - left <= 1) {
return;
}
// 定义mid 为了进行递归时候,表示递归的区间
int mid = (right + left) / 2;
_mergeSort(arr,left,mid);// 此处的区间我们采用的是前闭后开
_mergeSort(arr,mid,right);
merge(arr,left,mid,right);
}
public static void merge(int[] arr,int left,int mid,int right) {
// 进来先判断一下看看是否要给下边进行
if (left >= right) {
return;
}
// 定义一个数组
int[] temp = new int[right - left];
int tempIndex = 0;
int cur1 = left;
int cur2 = mid;
while (cur1 < mid && cur2 < right) {
// 要是cur1对应的值小,插入
if (arr[cur1] <= arr[cur2]) {
// 插入的时候,看好是把arr数组中的内容给temp中插入
temp[tempIndex] = arr[cur1];
cur1 ++;
tempIndex ++;
} else {// 将cur2插入到数组的后边
temp[tempIndex] = arr[cur2];
cur2 ++;
tempIndex ++;
}
}
// 循环结束后,判断谁还没有遍历完
while ( cur1 < mid) {
temp[tempIndex] = arr[cur1];
cur1 ++;
tempIndex ++;
}
while (cur2 < right) {
temp[tempIndex] = arr[cur2];
cur2 ++;
tempIndex ++;
}
// 最后再这个数组拷贝在原来的数组之中,注意拷贝的是left 下标,我们是不断的分成了区间,
// 每一个区间都是有左和右的,不能一股脑的拷贝给0号位置的下标
for (int i = 0; i < temp.length; i++) {
arr[left + i] = temp[i];
}
}
注意细节
- 区间选用的是前闭后开
- 在arr[cur1] 和 arr[xur2]比较的时候,要是cur1等于cur2也是要率先被插入的,还是为了保证稳定性
- 在比较完两个数组的时候,要进行判断看看是否有哪个数组有剩余,有的话将剩余的部分要插入进去
- 在将所有的数据插入完成后,不要忘了把插入好的数据在拷贝到原来的数组中去
性能分析
1.时间复杂度:O(n * log(n))
2.空间复杂度:O (n)