1、插入排序
思路:从位置1开始往后遍历,每个值插入到前面的合适位置。对于比他大的值,往后移一位,否则就插入
时间复杂度:O(n^2),空间复杂度:O(1)
public static void insertSort(int[] nums) {
//从1开始遍历
for (int i = 1; i < nums.length; i++) {
int temp = nums[i];
//将前面大于num【i】的数往后移一位,给要插入的数腾位子
int j = i - 1;
//如果该数是此时最小的,遍历到位置0就结束。或者如果遇到不比他大的,插入即可
while (j >= 0 && nums[j] > temp) {
nums[j + 1] = nums[j];
j--;
}
//插入该值
nums[j + 1] = temp;
}
}
2、冒泡排序
思路:从前往后遍历nums.length次,每次遍历把前后两个相邻的数大小排序。这样每一趟都会把剩余数组中最大的数排到最后面。直到有序。相当于冒泡泡
优化:对于原本有序,或者还没遍历完nums.length次就已经有序了,不需要再遍历了。所以每一次遍历用flag记录,如果没有交换,说明有序,返回即可
时间复杂度:O(n^2),空间复杂度:O(1)
public static void bubbleSort(int[] nums) {
//遍历nums.length-1次
for (int i = nums.length - 1; i > 0; i--) {
//每次设置一个flag记录是否发生交换
boolean flag = false;
//剩余未排序数组冒泡一趟。把最大的值交换到后面
for (int j = 0; j < i; j++) {
//需要交换
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
flag = true;
}
}
if (!flag) return;
}
}
3、快速排序法
思路:递归。
取剩余数组第一个值作为中枢,依次从后往前遍历,如果不比他小,跳过,否则把值放到最前面取值的位置。然后换成从前往后遍历。同理。交换遍历,最后指针相撞时结束。左右指针相等指向的位置把中枢值放到那里。中枢值把数组分成了左右两节。分别调用本方法进行排序。
递归结束条件:当传入的左指针>=右指针时,结束递归。
时间复杂度:O(nlog2n),空间复杂度:O(log2n)
```java
public static void quickSort(int[] nums, int left, int right) {
//递归一定要有结束条件
//当左指针等于右指针,说明数组只有一个元素。无需排序。当左指针大于右指针,说明前一个递归排序的中枢值就是第一个值。无序排序
if (left >= right) return;
//记录原始的左右开始的位置,用于后面递归调用
int cp_left = left;
int cp_right = right;
//用一个标志来判断此时指针是从后往前移动还是从前往后移动,刚开始右指针从后往前移动。
boolean isRight = true;
//记录中枢值
int temp = nums[left];
//当左右指针相撞时停止
while (left < right) {
//判断移动方向。类似于加锁的思想。cas
if (isRight) {
if (nums[right] >= temp) right--; //右指针的值不比中枢值小,不需要放到前面
else { //右指针的值比中枢值小,需要放到前面去
nums[left++] = nums[right];
isRight = false; //更新标志
}
} else { //否则左指针从前往后移动
if (nums[left] <= temp) left++;
else {
nums[right--] = nums[left];
isRight = true;
}
}
}
//最后当左右指针相等时,指向的位置就是中枢值的位置
nums[left] = temp;
//中枢值将数组分为左右两部分。递归调用本方法
quickSort(nums, cp_left, left - 1);
quickSort(nums, right + 1, cp_right);
}
4、选择排序法
思想:遍历数组nums.length-2次,每次找到最小的元素,和剩余数组的值交换。剩余数组为数组前面排好序后面部分.
优点是思路简单,代码简单。
时间复杂度:O(n^2),空间复杂度:O(1)
public static void selectSort(int[] nums) {
//遍历遍历数组nums.length-2次,因为最后一次只剩两个元素,找到小的放到前面。那最后一个数字就是最大的
for (int i = 0; i < nums.length-1; i++) {
//每次记录最小值和数组下标。因为要交换
int minVal = nums[i];
int minIndex = i;
for (int j = i; j < nums.length; j++) {
if (nums[j] < minVal) {
minVal = nums[j];
minIndex = j;
}
}
//交换
nums[minIndex] = nums[i];
nums[i] = minVal;
}
}
5、归并排序
基本思想
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
归并排序的最好,最坏,平均时间复杂度均为O(nlogn)
分而治之
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
代码实现
public static void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
最后
归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
6、计数排序
时间复杂度:O(N),空间复杂度:O(N)
02 基础版算法步骤
第一步:找出原数组中元素值最大的,记为max。
第二步:创建一个新数组count,其长度是max加1,其元素默认值都为0。
第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。
第四步:创建结果数组result,起始索引index。
第五步:遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。
第六步:返回结果数组result。
04 优化版
基础版能够解决一般的情况,但是它有一个缺陷,那就是存在空间浪费的问题。
比如一组数据{101,109,108,102,110,107,103},其中最大值为110,按照基础版的思路,我们需要创建一个长度为111的计数数组,但是我们可以发现,它前面的[0,100]的空间完全浪费了,那怎样优化呢?
将数组长度定为max-min+1,即不仅要找出最大值,还要找出最小值,根据两者的差来确定计数数组的长度。
详细内容:https://www.cnblogs.com/xiaochuan94/p/11198610.html
7堆排序
堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
堆排序基本思想及步骤
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
复杂度
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。
8希尔排序
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能。我们选择的增量序列{n/2,(n/2)/2…1}(希尔增量),其最坏时间复杂度依然为O(n2),一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为O(n3/2)。
图解排序算法(二)之希尔排序