排序算法 Java篇
最近再对数据结构进行复习,在以前的学习中还存在很多疑问,所以想通过这次复习将其弄懂,所以花了点时间进行整理,如若碰到不足之处,望指出。
阅读本节你将了解到以下内容:
冒泡排序算法思路与代码实现(以及优化)
选择排序算法思路与代码实现
插入排序算法思路与代码实现
直接插入
二分插入
Shell排序(主要逻辑还是插入排序)
快速排序算法思路与代码实现
归并排序算法思路与代码实现
基数排序算法思路与代码实现
堆排序算法思路与代码实现
各个算法之间的对比
排序算法 | 时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
直接插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
二分插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
Shell排序 | O(nlog2n) | O(n^2) | O(n) | O(1) | 不稳定 |
快速排序 | O(nlog2n) | O(n2) | O(nlog2n) | O(nlog2n) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
1. 冒泡排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
时间复杂度为O(n)是进行过优化的情况。可以查看优化的代码
冒泡排序是一种理解起来比较简单的排序算法,两两比较,将最大(或最小)的数不断后面进行放置即可。
代码示例
/**
* 1. 冒泡排序
* 思路:
* 1. 每次前后两个数进行比较,大的数往后面放
* 2. 不断的执行上面的过程
*/
public static int[] bubbleSort(int[] array){
//中间变量 用来帮助交换两个数的值
int tmp = 0;
//要比较多少轮
//时间复杂度O(n*n)
for (int i = 0; i < array.length; i++) {
//每轮需要比较的次数
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j+1]) {
tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
}
return array;
}
代码示例优化
public static int[] bubbleSort(int[] array){
//中间变量 用来帮助交换两个数的值
int tmp = 0;
boolean flag;
//要比较多少轮
//时间复杂度O(n*n)
for (int i = 0; i < array.length; i++) {
flag = false;
//每轮需要比较的次数
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j+1]) {
tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flag = true;
}
}
//当到这里 flag 为false的时候 说明其后面的元素是序的不需要在继续 了
if(false == flag){
return;
}
}
return array;
}
2.选择排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
选择排序的思路就是每次记录最大值的下标,在完成该次循环后再将这个数放到相对(每次进行比较元素最后的位置)后的位置
代码示例
/**
*2. 选择排序
* 思路:
* 1. 找出最大数的索引
* 2. 将最大数的索引与最后一个数进行交换
* @param array
* @return
*/
public static int[] selectSort(int[] array){
//每轮只交换一次 性能高于冒泡 虽然时间复杂度为O(n*n)
for (int i = 0; i < array.length; i++) {
//最大数的下标 默认为0
int maxNumIndex = 0;
//这里需要注意的就是 array的取值范围 用array.length - i表示
for (int j = 0; j < array.length - i; j++) {
//如果碰到更大的数 则更新下标
if (array[j] > array[maxNumIndex]) {
maxNumIndex = j;
}
}
//将其与最后一个数交换位置
int tmp = array[maxNumIndex];
//这里最后一个元素为array.length - i - 1
array[maxNumIndex] = array[array.length - i - 1];
array[array.length - i - 1] = tmp;
}
return array;
}
3.直接插入排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
这个比前两个复杂一点:
需要排序的数:4,3,8,2,7,1
选择第一个数放入集合 也就是4 结果如下
4 3,8,2,7,1
再次从数组中取出一个数 这里为 3 将其与前面已经放入集合且排好序的元素从后往前进行比较
4 比 3大,所以得到的结果如下
3,4 8,2,7,1
重复上面的操作得到
3,4,8 2,7,1
从上面可以看出当想新加入一个数放置的位置是当其大于集合中的某一个数的时候就放在其后面,开始在这个数后面的数顺位往后面移动。
具体可以结合代码进行理解
代码示例
/**
* 3. 直接插入排序
* 思路:
* 1. 取出一个数,这里假设有一个集合为m(已经是有序),让这个数与集合中的数做对比
* 2. 将这个数从后往前做对比,当这个数小于集合中的数,则集合中的数往后移动,如果小于,则这里为存放这个数的位置
* 3. 重复1,2
*
* @param array
* @return
*/
public static int[] insertSort(int[] array){
//算法时间复杂度为O(n * n)
for (int i = 1; i < array.length; i++) {
//记录下一个需要放入集合的数
int tmp = array[i];
int j;
for (j = i - 1; j >= 0; j--) {
if(tmp < array[j]){
//当前比较的数大于 下一个需要插入的数 则将当前比较的数往后移动一个位置
array[j + 1] = array[j];
}
//如果当前比较的数小于下一个需要插入的数 则退出循环
else{
break;
}
}
//由于退出循环时 j-- 了一次 因此在存放要插入的数据的时候j需要进行加一次
array[j + 1] = tmp;
}
return array;
}
4.二分插入排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
思路和上面直接插入大概一致,只是其在往集合中插入元素的时候使用的是二分查找插入,而不是一个个比较插入。具体可以结合代码
代码示例
/**
* 4. 二分法插入排序
* 思路:其与直接插入排序 相似 只是在插入的时候采用二分法进行查找插入的位置
* @param array
* @return
*/
public static int[] binaryInsertSort(int[] array){
for (int i = 1; i < array.length; i++) {
//保存要插入的数
int tmp = array[i];
//需要查找使用二分法查找的数组的最左方
int left = 0;
//最右方
int right = i - 1;
int mid;
while(left<=right){
mid = (left + right)/2;
//如果中间的值小于 要插入的值 说明这个数据处于 这个数组的后方法
if(array[mid] < tmp){
left = mid + 1;
}
//如果中间的值大于会等于要插入的值 说明这个数处于 这个数组的前方
else{
right = mid - 1;
}
}
//不管找到与否 这个数都应该在left后面的位置 所以这里需要将left及其后面的数向后移动
for (int j = i - 1; j >= left ; j--) {
array[j + 1] = array[j];
}
array[left] = tmp;
}
return array;
}
上面的几种实现其实不需要return 对于数组没有必要,可以选择删除。
5.Shell(希尔)排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(nlog2n) | O(n^2) | O(n) | O(1) | 不稳定 |
Shell排序的主要逻辑还是插入排序的思想。
不同点在于其有一个分量(自己看情况而定),比如下面一组数:
3,23,5,7,2,8,32,23,1,6
当分量为4(隔4个位置分为一组)的时候其可以分为下面几组数据:
3,2,1 结果:1,2,3 (这里主要还是交换了他们的位置),
23,8,6 结果:6,8,23
5,32 结果:5,32
7,23 结果:23,7
这个分量完成后的结果为:
分量为4的一轮排序结果:1,6,5,23,2,8,32,7,3,23,可以看出每组的最小元素都排到了前面。
再将分量进行细分,知道分量为1,即可以理解为上面的插入排序
代码示例
/**
* 5. 希尔排序
* 思想:
* 给定一个分量,每次对这个分量倍数上面的数进行排序
* 比如:当存在11个元素时,我们给定分量 为11/2 = 5
* 则后面的步骤为
* 对第 0,5,10位置元素进行插入排序 对1,6位置的元素进行选择排序 对2,7位置的元素进行排序 以此类推
* 后面对分量在进行细分 这里选择再除2 则为5/2=2
* 则其后面的步骤为
* 对0,2,4,6,8,10位置的元素进行插入排序 对1,3,5,7,9位置的元素进行插入排序
* 后面再将分量细分 再出二得到 2/2 = 1
* 即对所有位置的数进行插入排序
* 插入结束
*/
public static void shellSort(int[] array){
//用这个表示数组的长度 这样写的原因使得其不需要每次都在循环中调用array.length 处于加快程序运行速度考虑
int arrayLength = array.length;
//用length表示每次的分量
int length = array.length;
while (true){
//这里的length表示上面说的分量
length /= 2;
//这个循环表示再每一个循环下需要执行多少次插入排序
for (int i = 0; i < length ; i++) {
//下面为插入排序的逻辑
for (int j = i+length; j < arrayLength; j+=length) {
//利用一个局部变量保存需要加入到队列的数
int tmp = array[j];
int k;
for (k = j - length; k >= i; k-=length) {
//如果下一个要插入的数据小于数组中的数 则将数组中的数往后移动一个位置
if(tmp < array[k]){
array[k + length] = array[k];
}
//如果大于则直接退出
else{
break;
}
}
//由于退出循环时k-=m因此这里需要对其进行加上并将tmp放在这个位置
array[k + length] = tmp;
}
}
//这里需要设置退出循环所需要的条件,当length=1时,即分量为1的插入排序。其就是一个插入排序
//再完成分量为1的插入排序后 说明数组已经排好了顺序 因此需要退出循环
if(length == 1){
break;
}
}
}
6.快速排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(nlog2n) | O(n2) | O(nlog2n) | O(nlog2n) | 不稳定 |
快速排序简单来说就是选择一个基准点,一般选择第一个元素作为基准点。
将比这个基准点小的数放在这个数的左边,大的数放在这个基准数的右边。
可以结合下面的图片进行理解,但需要注意的是需要从右变开始,否则会出现错误的情况。
-
哨兵j出发,比基准点大继续走,比基准点小则停止。
- 当j停止后即其碰到一个比基准点小的数,其暂停了,这个时候i开始走,当其遇到一个比基准点大的数的时候i也停止。
-
当i,j同时停止的时候,说明其可以将使得自己停止的数进行交换。
- 通过上面的循环步骤就可以将所有的比基准点大的数据放在一边,小的数放在另一边。
- 将这个动作一值做下去直到其只有自生一个元素则停止。
- 结合代码理解更美味。
代码示例
/**
* 6. 快速排序
* @param array
* @param left
* @param right
* @return
*/
public static void quickSort(int[] array,int left, int right){
if(left > right){
return;
}
//选取一个哨兵
int i = left;
int j = right;
int tmp = 0;
while(i != j){
//这里先要判断这个 再判断下面的部分 否则会出现错误
if(array[left] <= array[j] && i < j){
j--;
}else if(array[left] >= array[i] && i < j){
i++;
}else{
//这个是当array[i] > array[left] && array[j] < array[left]执行
//所以这里交换这两个的值
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
}
}
//再上面进行大部分交换完后 将第一个元素放到相对的中间去
tmp = array[left];
array[left] = array[i];
array[i] = tmp;
//这里还需要对左右两边的数组进行
quickSort(array,left,i-1);
quickSort(array,i+1,right);
}
7.归并排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
归并排序总共分为两个步骤:
1. 将数组进行分割
2. 对已经排好序的数组进行合并(有兴趣的可以尝试将两个有序的链表进行合并为一个有序的链表)
3. 分别完成上面两个步骤应该难度不大。对于归并排序自己感觉存在问题的就是对于参数的把握,而这个需要进行更为细节的考虑。
4. 参考下面代码以及注释应该就可以很好的理解了。
代码示例
/**
* 归并排序:
* 思想:
* 1.对数组递归分割。
* 2.将拍好序的数组进行合并这里使用的是mergeArray()方法。
* 3.将排的数组放回原数组中。
* @param array
* @param start
* @param end
*/
public static void mergeSort(int[] array,int start,int end){
if(start >= end){
return;
}
int mid = (start + end)/2;
mergeSort(array,start,mid);
mergeSort(array,mid+1,end);
mergeArray(array,start,mid,end);
}
/**
* 这个函数需要将 array数组指定范围(start-end)内的元素进行排序
* @param array
* @param start
* @param mid
* @param end
*/
public static void mergeArray(int[] array,int start,int mid,int end){
//需要用一个数组来存放 归并后的元素
int[] tmp = new int[end - start + 1];
//第一个数组的第一个元素
int i = start;
//第二个数组的第一个元素
int j = mid + 1;
//用于tmp存放的下标索引
int count = 0;
while(i <= mid && j <= end ){
//如果 前面的那段小于后面那段 则将其 放入tmp中
if(array[i] < array[j]){
tmp[count] = array[i];
i++;
}
//否则 则将后面那段放入 tmp中
else{
tmp[count] = array[j];
j++;
}
count++;
}
//这里需要判断是谁越界退出的循环
//如果check == 1则说明是因为length1全部完成 否则 length2完成
int check = (i == mid + 1)? 1 : 2;
if (check == 1){
for (int k = j; k <= end; k++) {
tmp[count] = array[k];
count++;
}
}else{
for (int k = i; k <= mid ; k++) {
tmp[count] = array[k];
count++;
}
}
copyArray(array,tmp,start);
}
/**
* 将array数组从位置start开始将tmp里面的值复制过去
* @param array
* @param tmp
* @param start
*/
public static void copyArray(int[] array,int[] tmp,int start){
int length = tmp.length;
for (int i = 0; i < length; i++) {
array[start] = tmp[i];
start++;
}
}
8.基数排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 |
1. 获得是个桶,其对应着0,1,2,3,4,5,6,7,8,9。
2. 按顺序得到每个数的个位,将这个数放入与这个数个位相等的桶里(如果这个数为99,其个位为9 ,则将99这个数放入9号桶里)。
3. 将放入桶里的数按从0号-9号桶的按顺序取出。
4. 再将上面取出的数获得其十位(怎么获得其十位上的数自己可以想想,可参考代码)。后面的步骤和2,3一样。
5. 如果有百位,千位,处理方式相同
代码示例
/**
* 基数排序 :
* 思想:
* 1. 先获得10个桶(也就是数组)对应着0,1,2,3,4,5,6,7,8,9。10个数字
* 2. 循环将每一个数字进行个位、十位、百位.....进行逐个求出并放入对应的桶中
* 3. 每对一个位进行求和完毕则将数组取出后重新放入
*
*
* 基数排序不能处理负数的情况
* 代价大的解决方案:找出最小的负数 对其数组里面的每个元素加上一个绝对值大于最小负数的正数
* 使得数组里面的数全为正数后排序完成再减去这个正数
* @param array
*/
public static void radixSort(int[] array){
//先找出最大的数 看其总共需要求几轮值
int max = Integer.MIN_VALUE;
for (int i = 0; i < array.length; i++) {
if(max < array[i]){
max = array[i];
}
}
//获取最大值的长度
int maxLength = (max + "").length();
//数组的长度
int arrayLength = array.length;
//定义桶 桶只有10个 其容量最多只有 arrayLength个
int[][] tmp = new int[10][arrayLength];
//记录每一个桶中存的元素的个数 总共需要记录十个位置
int[] count = new int[10];
//最多循环最大值长度次循环
for (int i = 0,n = 1; i < maxLength; i++,n*=10) {
//这里需要对每个数进行个位 、十位、百位进行求值并放入数组中
for (int j = 0; j < arrayLength; j++) {
//对每个数进行除n%10可以求出其对应位的值
int ys = array[j]/n%10;
//求出余数说明其是放在 第ys个桶的位置 这里需要一些桶 同时需要这个桶中存了多少个数
tmp[ys][count[ys]] = array[j];
//桶中存放的数多了一个
++count[ys];
}
int pos = 0;
//执行过一个位的存放后需要对其按顺序取出并放入原数组中
for (int j = 0; j < tmp.length; j++) {
//每个桶中存放元素的个数
for (int k = 0; k < count[j]; k++) {
array[pos] = tmp[j][k];
//将当前位置的值置零以便下次循环
tmp[j][k] = 0;
pos++;
}
//将当前位置的元素个数置零 以便下次循环使用
count[j] = 0;
}
}
}
9.堆排序
时间复杂度 (平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
堆排序比较复杂,上图(从别人哪里tou来的,嘻嘻(源地址)):
堆排序是选择排序的一种,通过建立堆将其最大(最小)的元素找出。
其过程主要也分为两个步骤:
- 建堆
- 调整堆
这个单纯靠我这里的这部分可能还不能够理解(可以取查找单独对堆排序进行讲解的教程进行参考 堆排序视频)。
代码示例
/**
* 堆排序
* @param array
*/
public static void heapSort(int[] array){
//数组的长度
int length = array.length;
int index = (length - 2) / 2;
for (int i = index; i >= 0; i--) {
//各个局部建堆 最后形成一个大堆
maxHeap(array,length,i);
}
//不断将对顶的元素和堆尾的元素进行交换 这个过程中需要调整需要加入堆中的元素的个数
for (int i = length - 1; i > 0; i--) {
int tmp = array[i];
array[i] = array[0];
array[0] = tmp;
//将最大的数移到最后面之后就不需要再将其进行比较了 因此size 变小了
//但由于这次交换 堆被破坏了 因此需要从被破坏的地方重新进行堆调整
maxHeap(array,i,0);
}
}
/**
*
* @param array 要建立堆的数组
* @param size 参与建堆的数组大小 前多少个
* @param index 开始调整的位置
*/
public static void maxHeap(int[] array,int size,int index){
//当前位置的左边元素的索引 这个是完全二叉树的特性
//左边节点的下标等于 父节点的 2*fatherIndex + 1
int lIndex = 2 * index + 1;
//右边元素的下标
int rIndex = 2 * index + 2;
//下面段代码的目的在于找到 父 左 右三个节点中最大的元素来当父亲
int max = index;
//这里判断size是保证其后面不会出现越界的情况
if(lIndex < size && array[max] < array[lIndex]){
max = lIndex;
}
if(rIndex < size && array[max] < array[rIndex]){
max = rIndex;
}
//如果位置改变了 则交换位置
//没改变则没有必要
if(max != index){
//将最大的元素放到父亲的位置
//保存父节点的位置
int tmp = array[index];
//将最大元素覆盖父节点的位置
array[index] = array[max];
array[max] = tmp;
//当因为调节了前面的元素导致了后面的堆进行了破坏 需要对后面的堆再进行调整
maxHeap(array,size,max);
}
}
10.总结
其实各个排序算法理解起来并不算太难,自己再这次复习的过程中首先是从简单的排序进行理解,其中包括冒泡排序,直接选择排序,直接插入排序,二分插入排序。在这段时间接触了许多关于递归(牛课剑指offer)的问题,所以对复杂的排序算法理解也不算太难。总的来说对排序算法的学习需要一个过程,静心多练习。
11.参考链接
https://blog.csdn.net/sd09044901guic/article/details/80613053
https://blog.csdn.net/qq_41891257/article/details/85245127
('')第一篇博客,不喜勿喷。