目录
实现元素交换的代码
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
-
选择排序
选择排序的思想是,每次都遍历遍数组,选择最小的放在第一位,再从第二位开始遍历,选择第二小的放在第二位,依次类推。选择排序的时间复杂度为O(n^2)
注意:不要一找到比指定位置小的元素就进行交换,这样操作耗时,而是记录最小的索引,在遍历完之后只进行依次交换操作。
实现代码:
public static void selectionSort(int[] arr, int n) {
for (int i = 0; i < n; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
空间复杂度:需要额外的一个空间保存最小值用于交换,空间复杂度为O(1)。
时间复杂度:每次都要遍历剩下所有的元素,所以总的时间复杂度O(n^2),最坏和最好情况下也是O(n^2),所以平均时间复杂度是O(n^2)
稳定性:不稳定,例如在一次比较中,某个位置是当前最小元素需要交换到的位置,而当前最小的元素在和这个元素相等的元素的后面,当进行交换之后,稳定性就被破坏了。5 8 5 2 9-》2 8 5 5 9。
-
冒泡排序
冒泡排序的思想是:比较相邻的两个元素,若前一个元素比后一个元素大,就交换位置,经过一次循环后,最大值就到达最后的位置,再循环一次第二大的值就到达了倒数第二的位置,以此类推,直到数组有序。冒泡排序的时间复杂度也为O(n^2)
public static void bubbleSort(int[] arr){
//外层循环控制循环次数,即经过几次冒泡后就可以得到顺序的数组,没经过一次冒泡之后,需要遍历的数组元素减1,因为已经将其放入了正确的位置
for(int i=0;i<arr.length-1;i++){
//内层循环用来遍历数组
for(int j=0;j<arr.length-i-1;j++){
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
空间复杂度:只需要一个值得空间进行交换,时间复杂度为O(1)
时间复杂度:最坏情况:数组为逆序,每一轮遍历都要进行交换,时间复杂度为O(n^2).最好情况:序列为正序,遍历一次没有交换位置,排序结束,所以为O(n).平均时间复杂度O(n^2)。
稳定性:稳定,只有相邻的元素才会交换位置,当两个元素值相等的时候不会交换位置。如果没有相邻,如果通过一系列操作让两个元素相邻后也不会进行交换。
-
插入排序
插入排序的思想是,遍历一边数组,将每个数与其前面的数进行比较,将其放入正确的位置上。插入排序的优点是再找到第一个比它小的元素的时候可以停止退出循环,停止遍历。对于近乎有序的数组来说,插入排序的效率更高。插入排序的时间复杂度为O(n^2)
代码实现:
public static void insertSort(int[] arr){
//外层循环遍历数组,每次找到比目标元素更大的数之后往后移一个
for(int i=0;i<arr.length;i++){
//将要进行插入排序的元素
int e = arr[i];
//元素将要插入的索引
int j;
for(j=i;j>0;j--){
if(e<arr[j-1]){
arr[j] = arr[j-1];
}else{
break;
}
}
arr[j]=e;
}
}
空间复杂度:需要一个空间保存待插入的值,空间复杂度为O(1)
时间复杂度:最坏时间复杂度,逆序时,需要将每一个元素都插入到已经排序好序列最前面,排好序的序列整体往后移一个,时间复杂度为O(n^2)。最好时间复杂度:顺序时,遍历一遍没有移动就排好序,时间复杂度O(n)。平均时间复杂度:O(n^2)
稳定性:稳定的。如果有两个相同的元素,第一个元素排好序之后,第二个进行插入的时候,会插入到第一个小于等于它的元素后面,所以是稳定的。
-
希尔排序
希尔排序是插入排序的一种。希尔排序的中心思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。 * 先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1), * 即所有记录放在同一组中进行直接插入排序为止。希尔排序又叫缩小增量排序。
代码实现:
public static void shellSort(int[] arr){
//外循环控制增量
for(int i=arr.length/2;i>0;i /= 2){
//从第i个元素开始,将其插入到对应的位置上
for(int j=i;j<arr.length;j++){
int e = arr[j];
//k记录元素e将要插入的位置
int k;
//遍历元素e前的元素,若比e大将元素以gap向后移,找到e应该插入的位置。
for(k=j;k>0;k -= i){
if(k-i>=0&&e<arr[k-i]){
arr[k] = arr[k-i];
}else{
break;
}
}
arr[k] = e;
}
}
}
希尔排序是在一定的间隔进行插入排序,然后逐步缩小间隔,最后间隔为1时,相当于插入排序,这个时候数组已经基本有序了。无需进行过多的移动和比较。
空间复杂度:O(1)
时间复杂度:比较复杂
稳定性:不稳定。虽然在每一个分组中插入排序时稳定的,但是不同分组中可能有相同的数字,进行插入排序后可能破坏稳定性。
-
归并排序
归并排序思想是,将程序先分成两部分,再分成4部分,再分成8部分,一直到一部分只存在一个元素,然后进行归并。对于分开的两部分数,用第三个辅助数组来进行排序,i,j,k分别指向两部分数组和最终排序好的数组。将两个数组中较小的往最终数组中放入,再接着放入次小的。
算法实现:
public static void mergeSort(int[] arr){
merge_sort(arr,0,arr.length-1);
}
/**
* l和r表示在[l...r]范围内进行归并排序
* @param arr
* @param l
* @param r
*/
private static void merge_sort(int[] arr,int l,int r){
if(l>=r){
return;
}
int mid = l+(r-l)/2;
merge_sort(arr,l,mid);
merge_sort(arr,mid+1,r);
merge(arr,l,mid,r);
}
/**
* 将归并排序的两部分进行整合,使这两部分组成一个有序的数组
* @param arr
* @param l
* @param mid
* @param r
*/
private static void merge(int[] arr,int l,int mid,int r){
int[] res = new int[r-l+1];
//将数组的值赋值给res
for(int i=l;i<=r;i++){
res[i-l] = arr[i];
}
//指向res的索引
int k;
//指向左半部分的索引
int i=l;
//指向右半部分的索引
int j=mid+1;
//用复制的数组进行归并排序来填满原来的数组
for(k=0;k<r-l+1;k++){
if(res[i]<res[j]){
arr[k]=res[i];
}else if(i>mid){
j++;
}else if(res[i]>res[j]){
arr[k]= res[j];
j++;
}else if(j>r){
i++;
}
}
}
自底向上进行归并排序
/**
* 自底向上进行归并排序
* @param arr
*/
public static void mergeSortBU(int[] arr){
//sz代表进行归并排序的数组的大小,从1开始每次都翻倍
for(int sz=1;sz<=arr.length;sz=sz+sz){
//从元素0开始,对两个sz大小的数组进行归并排序
//i+sz是为了保证第二部分存在,对[i...i+sz-1]和[i+sz....i+2*sz]内的元素进行归并排序
for(int i=0;i+sz<arr.length;i += sz+sz) {
//可能右半部分的元素不足sz个要进行判断
merge(arr, i, i + sz - 1, min(i + 2 * sz - 1, arr.length - 1));
}
}
}
空间复杂度:将序列划分为两个子数组,一直递归到每个子数组只有一个元素,待子数组有序之后进行归并。copy原数组,然后将copy数组进行归并。每次从两个有序子数组中较小的数放入原数组中。空间复杂度O(n)
时间复杂度:O(nlogn)。时间花在划分序列和合并序列上。合并将两个子数组合并成有序数组,时间复杂度为O(n).递归数最多只有O(log2n)层,所以时间复杂度为O(nlogn)。最好最坏和平均都为O(nlogn)
稳定性:递归最后会让子数组只剩下一个元素,然后进行递归的合并,合并的过程中,如果两个数相等,会让前面的数先放入有序的数组中,后面的数后放入有序数组中。
-
快速排序
找一个基准数,将大于这个基准数的放入他的左边,将小于这个数的放入他的右边,再对左右两边的数进行同样的操作。直到左边的数全为小于基准数的数,右边的数全为大于基准数的数。然后对左右两边重复以上操作。
具体的操作为:随机选取一个基准数,将其与第一个元素交换位置,遍历数组,若值大于基准值不做任何改变,若值小于基准值,则和arr[j+1]交换位置,j为大于基准值的开始索引,即在[l+1...j]范围内值小于基准值,[j+1...r]范围内,值大于基准值,最后将数组首位的基准值与arr[j]位置的元素交换位置。然后对[l...j-1],[j+1...r]两个数组重复以上操作。
快速排序参考:https://blog.youkuaiyun.com/qq_20011607/article/details/82357239动画演示
/**
* 快速排序.快速排序的思想是,
*/
private static void quickSort(int[] arr){
quick_sort(arr,0,arr.length-1);
}
/**
* 在[l...r]范围内进行快速排序
*/
private static void quick_sort(int[] arr,int l,int r){
if(l>=r){
return;
}
//分割,返回分割点
int p = partition(arr,l,r);
quick_sort(arr,l,p-1);
quick_sort(arr,p+1,r);
}
private static int partition(int[] arr,int l,int r){
//产生基准值
Random random = new Random();
int index = random.nextInt(r-l+1)+l;
//将基准值移动到首位
int v = arr[index];
swap(arr,l,index);
//j记录小于基准值的最后一个元素,即在[l...j]内小于基准值,[j+1...r]内大于基准值
int j = l;
for(int i=l+1;i<=r;i++){
if(arr[i]<v){
//如果发现了比基准值小的值,将其与j+1位置的值交换位置,并且让j++;
swap(arr,i,j+1);
j++;
}
}
//循环完成之后将首位的基准值移到j位置
swap(arr,l,j);
return j;
}
双路快排:当存在大量与基准数相同的数的时候,要么全在左边,要么全在右边,可能导致左右两边一边数很多,一边数很少。不均衡。解决方法是使用双路快排。
双路快排的思想是:设立两个区大于等于基准数区和小于等于基准数区,让等于基准数的数在左右两边均匀分布
需要两个索引i和j,在[l...i-1]内<=v,在[j...r]内>=v;从两头开始遍历数组,若arr[i]<=v则i++,直到>v.若arr[j]>=v,则j--,直到arr[j]<v,j从length-1开始计数,然后将arr[i]与arr[j]交换位置。循环到l>=r结束。返回i为基准的索引位置。
算法实现:
/**
* 双路快排,设置大于等于区和小于等于区,让等于基准数的数在左右两边均匀分布
*/
private static void quickSort2(int[] arr){
quick_sort2(arr,0,arr.length-1);
}
private static void quick_sort2(int[] arr,int l,int r){
if(l>=r){
return;
}
int p = partition2(arr,l,r);
quick_sort2(arr,l,p-1);
quick_sort2(arr,p+1,r);
}
private static int partition2(int[] arr,int l,int r){
//产生基准值
Random random = new Random();
int index = random.nextInt(r-l+1)+l;
int v= arr[index];
swap(arr,l,index);
//在[l...i]范围内<=v
int i=l;
//在[j...r]范围内>=v
int j=r;
while (true){
while (i<=r&&arr[i]<=v){
//i一直加加直到找到>v的值
i++;
}
while (j>1&&arr[j]>=v){
//j一直减到找到<v的数
j--;
}
if(i>=j){
break;
}
swap(arr,i,j);
}
swap(arr,l,i-1);
return i-1;
}
三路快排:从两端向中间挺进,分三个区,大于区,等于区,小于区。
基本思想:需要三个索引,第一个lt,在[l+1...lt]范围为小于区,第二个i,遍历数组的索引,第三个gt,在[gt...r]内为大于区。最终在[lt+1...gt-1]为等于区,将第一个基准元素调整位置之后,在[l...lt-1]为小于区,[lt...gt-1]为等于区,[gt...r]为大于区。编程思想为,从基准元素后面一个开始遍历元素,开始的时候lt=l,gt=r+1保证小于区和等于区的元素都不存在。从l+1开始遍历元素,当元素小于基准值时,和lt+1位置的元素进行交换,lt++,i++;元素等于基准值时,i++,元素大于基准值时,gt--,再和gt位置的元素交换位置,注意此时i不进行自增操作,因为交换后的元素是没有经过处理的。然后对小于区的元素和大于区的元素进行同样的递归操作。
算法实现:
public static void quickSort3(int[] arr){
quick_sort3(arr,0,arr.length-1);
}
/**
* 因为需要返回两个两个索引值,直接在sort中进行partition
* @param arr
* @param l
* @param r
*/
public static void quick_sort3(int[] arr,int l,int r){
if(l>=r){
return;
}
//小于区结束的索引
int lt=l;
//大于区开始的索引
int gt=r+1;
//遍历数组的索引
int i=l+1;
//partition
//产生基准值
Random random = new Random();
int index = random.nextInt(r-l+1)+l;
int temp = arr[index];
swap(arr,l,index);
while (i<gt){
if(arr[i]<temp){
//小于基准值时,将其与小于区后面的一个元素交换
swap(arr,i,lt+1);
i++;
lt++;
}else if(arr[i]==temp){
i++;
}else{
//大于基准值时将其与大于区的前一个元素交换
swap(arr,i,gt-1);
gt--;
}
}
//将基准元素和第lt个元素交换位置
swap(arr,l,lt);
quick_sort3(arr,l,lt-1);
quick_sort3(arr,gt,r);
}
空间复杂度:就地快速排序的空间复杂度是O(1)。每次递归时都要保持一些数据,最优的情况下,每次平分一办O(logn)。最坏情况下,退化为冒泡排序O(n)
空间复杂度:最坏,每次选取的基准值都是最大值或最小值。最好,每次都能平分整个序列,时间复杂度O(nlogn)
稳定性:不稳定。选取基准元素时,将基准元素放在第一个位置,若第一个位置的元素和后面某个元素相同,基准元素又在那个元素后面,将第一个元素和基准元素交换位置后,进行排序时,两个元素的顺序被打乱。
-
堆排序
二叉堆(最大堆):任何一个节点的子节点不大于其父节点,每一个节点的子节点也是最大堆。完全二叉树
堆排序的主要思想是:首先将待排序的数组变成一个最大堆:从最后一个非叶子节点开始,从右至左,将每个节点进行sift down操作,从而将数组变成一个最大堆。在数组中,从索引0开始,对于每一个节点i:其父节点的索引为(i-1)/2,其左孩子的索引为2*i+1,其右孩子的索引为2*i+2。最后一个非叶子节点的索引为length/2-1.将一个无序堆构造成一个最大堆
经过以上步骤之后将一个无序序列构建成了一个最大堆。
接着将堆顶元素(这个序列的最大值)与末尾元素进行交换,这时最末尾的就为整个数组的最大值。接着讲去除最后一个节点的树进行堆重建,使最大的值到达堆顶。将堆顶第二大的数与倒数第二个节点交换位置,再将剩余的堆进行重构,如此一直进行交换重构。就形成了一个从小到大的序列,堆数组进行了从小到大的排序。
注意:在进行交换之后,剩下的参与排序的节点数要减1,即进行堆重构之后,依旧是将堆顶元素与最后一位进行交换。
堆排序的时间复杂度为O(nlogn).
代码实现:
package 算法.排序算法;
import java.util.Random;
class HeapSort{
public static void heapSort(int[] arr) {
//构建最大堆
//从第一个非叶子节点开始,从右至左进行调整,第一个非叶子节点为(length-1-1)/2=length/2-1,将原数组构建为一个最大堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
for (int j = arr.length - 1; j >= 0; j--) {
//将最后一个元素与第一个元素进行交换
swap(arr, 0, j);
//剩余的元素构建最大堆
adjustHeap(arr, 0, j);
}
}
/**
* 调整最大堆
* i代表需要调整的节点,length代表需要调整的堆的长度,
*/
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];
//从i节点的左孩子开始遍历,比较右孩子和左孩子的值,和较大的进行交换。
for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
//如果有孩子存在且右孩子比左孩子大,将k指向右孩子
if (k + 1 < length && arr[k + 1] > arr[k]) {
k++;
}
//将arr[i]与它的左右孩子中最大的那个进行比较
if (arr[k]>temp) {
//若arr[i]<arr[k]将其交换
swap(arr, i, k);
//若进行了交换,还要判断交换后以temp为节点的子树需不需要调整,此时判断的节点坐标为k,要将i置为k。
i = k;
} else {
break;
}
}
}
/**
* 交换arr数组中索引为i和索引为j的两个元素
*
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
Random random = new Random();
int[] arr = new int[20];
for (int i = 0; i < 20; i++) {
arr[i] = random.nextInt(20);
}
for (int j = 0; j < arr.length; j++) {
System.out.print(arr[j] + " ");
}
System.out.println();
HeapSort.heapSort(arr);
for (int j = 0; j < arr.length; j++) {
System.out.print(arr[j] + " ");
}
}
}
空间复杂度:O(1)用于元素交换
时间复杂度:对于n个数据有n-1次建堆的过程,建堆操作的时间复杂度为log2n。所以最好最坏,平均都为nlogn。
稳定性:不稳定。堆排序是在父节点和两个子节点中选取最大或最小的元素进行交换,这样的操作不会破坏稳定性。但是如果其中的一个子节点在和它的子节点进行比较并交换了子后,就破坏了稳定性。