java算法基础——排序算法

一、排序算法

以下代码的测试数据都是:

int[] arr = new int[] {10, 5, 3, 8, 2, 4, 9, 1, 7, 6};

1、冒泡排序(Bubble Sort)

其核心思想是:对于一组需要排序的数字,依次将个位置上的数字与逐一与其之后的数字进行比较,如果他们的顺序错误就把他们交换过来。 这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端。

public static void bubbleSort1(int[] arr) {
    int count1 = 0;//记录循环次数
    int count2 = 0;//记录交换次数
    for (int i = 0; i < arr.length - 1; i++) {
        count1++;
        for (int j = 0; j < arr.length - 1 - i; j++) {
            count1++;
            if (arr[j] > arr[j + 1]) {
                count2++;
                swap(arr, j, j + 1);
            }               
        }           
    }
    System.out.println("bubbleSort1:循环次数" + count1 + ",交换次数:" + count2);
}
public static void bubbleSort2(int[] arr) {
    int count1 = 0;//记录循环次数
    int count2 = 0;//记录交换次数
    for (int i = 0; i < arr.length - 1; i++) {
        count1++;
        for (int j = i; j < arr.length - 1; j++) {
            count1++;
            if (arr[i] > arr[j + 1]) {
                count2++;
                swap(arr, i, j + 1);                  
            }
        }           
    }
    System.out.println("bubbleSort2:循环次数" + count1 + ",交换次数:" + count2);
}
private static void swap(int arr[],int i,int j) {
    int temp =arr[i];
    arr[i] = arr[j];
    arr[j] = temp; 
}

输出结果为:
bubbleSort:循环次数54,交换次数:26。

算法时间复杂度分析:

对于排序算法的时间复杂度分析,要从2个角度考虑,一个是比较的次数,另一个交换的次数。对于n个元素的数组,需要进行 n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1)。

比较的次数的时间复杂度:

第一趟:n-1次比较,第二趟:n-2次比较,……,第n-1趟,1次比较。所以算出:
这里写图片描述

交换次数的时间复杂度:

在比较过程中,交换不是必须的,只有在顺序不对的情况下,才会交换。如果数组是升序的,但是我们希望降序排列,那么每一次比较都需要进行交换,这个时候达到交换的最大次数,且每次比较都必须移动记录三次来达到交换记录位置。所以算出:
这里写图片描述

综上,因此冒泡排序总的平均时间复杂度为O(N的平方)。

算法稳定性分析:

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较的是相邻的两个元素,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个元素相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

2、选择排序(Selection Sort)

选择排序是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

public static void selectSort(int[] arr) {
    int count1 = 0;//记录循环次数
    int count2 = 0;//记录交换次数
    for (int i = 0; i < arr.length - 1; i++) {
        int index = i;
        count1++;
        for (int j = i + 1; j < arr.length; j++) {
            count1++;
            if (arr[index] > arr[j]) {
                index = j;
            }
        }
        if (index != i) {
            count2++;
            swap(arr, index, i);
        }
    }
    System.out.println("selectSort:循环次数" + count1 + ",交换次数:" + count2);
}
private static void swap(int arr[],int i,int j) {
    int temp =arr[i];
    arr[i] = arr[j];
    arr[j] = temp; 
}

输出结果为:
bubbleSort:循环次数54,交换次数:6。

算法时间复杂度分析:

选择排序只是比冒泡排序优化了一点点,比较的次数没有变,但是减少了交换的次数。

算法稳定性分析:

选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面),所以相同元素的前后顺序发生改变。

3、插入排序(Insertion Sort)

插入排序(Insert Sort)将待排序的数组分为2部分:有序区,无序区。其核心思想是每次取出无序区中的第一个元素,插入到有序区中。 有序与无序区划分,就是通过一个变量标记当前数组中,前多少个元素已经是局部有序了。

插入排序根据具体实现方式又分为:直接插入排序,二分插入排序(又称折半插入排序),链表插入排序,希尔排序(又称缩小增量排序)。

算法时间复杂度分析:

第1趟:最多需要比较1次,第2趟:最多需要比较2次,……,第n-1趟:最多需要比较n-1次。所以最多需要比较n*(n-1)/2次。

冒泡排序也是需要比较n*(n-1)/2次,但是二者不同之处在于,冒泡排序肯定是需要比较n*(n-1)/2次,插入排序只有在最坏的情况下才会需要n*(n-1)/2次,回顾上例中的出现的break,当我们发现某个元素不符合时,就直接跳出,有序区之前的元素都不会再比较了,从概率的角度来说,实际上只需要和有序区中一半的元素进行比较,因此需要除以2,即插入排序比较的平均时间复杂度是n*(n-1)/4,所以有的时候我们会看到 插入排序比冒泡排序效率高一倍 的说法。

算法稳定性分析:

属于稳定排序的一种(通俗地讲,就是两个相等的数不会交换位置)。

1)直接插入排序

public static void straightInsertionSort(int[] arr){
    int count1 = 0;//记录循环次数
    int count2 = 0;//记录交换次数
    for (int i = 1; i < arr.length; i++) {//从无序区第一个元素开始迭代
        count1++;
        int temp = arr[i];//记录无序区第一个元素的值
        int insertIndex = i;//记录在有序区中插入索引的位置,刚开始就设置为自己的位置
        for (int j = i - 1; j >= 0; j--) {//从有序区最后一个元素开始比较
            count1++;
            if (temp < arr[j]) {
                count2++;
                arr[j + 1] = arr[j];
                insertIndex--;//有序区每移动一次,将插入位置-1
            }else {
                break;//有序区当前位置元素<=无序区第一个元素,那么之前的元素都会<=,不需要继续比较
            }
        }
        arr[insertIndex] = temp;
    }
    System.out.println("straightInsertionSort:循环次数" + count1 + ",交换次数:"
                + count2);
}

输出结果为:
straightInsertionSort:循环次数40,交换次数:26。

2)二分插入排序

二分插入排序与直接插入排序的区别是,直接插入排序是迭代有序区中的每一个数据项与无序区中的第一个元素进行比较,二分插入排序实际上是充分利用了有序区的特定,我们知道,对于一个有序的数组,我们可以利用二分查找快速定位某个数字应该插入的位置,在定位了这个位置之后,只需要将这个位置以及之后的元素右移一位,将腾出来的位置直接插入无序区中的第一个元素即可,减少了比较次数。

public static void binaryInsertionSort(int[] arr) {
    int count1 = 0;//记录循环次数
    int count2 = 0;//记录交换次数
    for (int i = 1; i < arr.length; i++) {// 从无序区第一个元素开始迭代
        count1++;
        int temp = arr[i];// 记录无序区第一个元素的值
        int index = binarySearch(arr, temp, 0, i);
        for (int j = i; j > index; j--) {
            count2++;
            arr[j] = arr[j - 1];
        }
        arr[index] = temp;
    }
    System.out.println("binaryInsertionSort:循环次数" + count1 + ",交换次数:" + count2);
}
private static int binarySearch(int[] arr, int target, int start, int end){
    int range = end - start;
    if (range > 0) {
        int mid = (start + end) / 2;
        if (arr[mid] > target) {
            return binarySearch(arr, target, start, mid - 1);
        }else if (arr[mid] < target) {
            return binarySearch(arr, target, mid + 1, end);
        }else {
            return mid + 1;
        }
    }else {
        if (arr[start] > target) {
            return start;
        }else {
            return start + 1;
        }
    }
}

输出结果为:
binaryInsertionSort:循环次数9,交换次数:26。

4、归并排序(Merge Sort)

归并排序是分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列之间有序。将两个有序数组合并成一个有序数组,称为二路归并(binary merge)。

归并排序的思想:

将一个数组拆分成两半,分别对每一半进行排序,然后使用合并(merge)操作,把两个有序的子数组合并成一个整体的有序数组。我们可以把一个数组刚开始先分成两,也就是2个1/2,之后再将每一半分成两半,也就是4个1/4,以此类推,反复的分隔数组,直到得到的子数组中只包含一个数据项,这就是基值条件,只有一个数据项的子数组肯定是有序的。

排序过程如图:
这里写图片描述

@Test
public void test(){
    int[] arr = new int[] { 10, 5, 3, 8, 2, 4, 9, 1, 7, 6 };
    mergeSort(arr, 0, arr.length - 1);
    System.out.println("mergeSort:循环次数" + count1 + ",交换次数:" + count2);
}
private static int count1 = 0;//记录循环次数
private static int count2 = 0;//记录交换次数
/*
 * 递归拆分数组
 */
public static void mergeSort(int[] arr, int begin, int end) {
    if (end > begin) {
        int mid = (begin + end) / 2;
        mergeSort(arr, begin, mid);
        mergeSort(arr, mid + 1, end);
        merge(arr, begin, end);
    }
}
/*
 * 拆分后进行排序比较
 */
private static void merge(int[] arr, int startIndex, int endIndex) {

    int mid = (startIndex + endIndex) / 2;// 将数组分成两个数组,两个数组内部已排序
    int leftStartIndex = startIndex;// 左边数组,已排序
    int rightStartIndex = mid + 1;// 右边数组,已排序
    int hasMerge = 0;
    int temp[] = new int[endIndex - startIndex + 1];// 创建临时数组还存储比较后的数据
    while (leftStartIndex <= mid && rightStartIndex <= endIndex) {// 两个数组依次比较
        count1++;
        if (arr[leftStartIndex] < arr[rightStartIndex]) {
            temp[hasMerge++] = arr[leftStartIndex++];
        } else {
            temp[hasMerge++] = arr[rightStartIndex++];
        }
    }
    while (leftStartIndex <= mid) {// 如果左边数组还有余值,依次放入临时数组中
        count1++;
        temp[hasMerge++] = arr[leftStartIndex++];
    }
    while (rightStartIndex <= endIndex) {// 如果右边数组还有余值,依次放入临时数组中
        count1++;
        temp[hasMerge++] = arr[rightStartIndex++];
    }
    hasMerge = 0;
    while (startIndex <= endIndex) {// 将排序后的临时数组拷贝进原数组中
        count2++;
        arr[startIndex++] = temp[hasMerge++];
    }
}

输出结果为:
mergeSort:循环次数34,交换次数:34。

算法时间复杂度分析:

归并排序的最好、最坏和平均时间复杂度都是O(nlogn),而空间复杂度是O(n),比较次数介于(nlogn)/2和(nlogn)-n+1,赋值操作的次数是(2nlogn)。

算法稳定性分析:

归并排序算法比较占用内存,但却是效率高且稳定的排序算法。

5、快速排序(Quick sort)

毫无疑问,快速排序(quick sort)是最流行的排序算法,因为有充足的理由,在大多数情况下,快速排序都是最快的,执行时间为O(N*logN) 级。

快速排序算法本质上通过把一个数组划分(patition)为两个子数组,然后递归地调用自身,将子数组划分为更细的子数组,为每一个子数组进行快速排序来实现的。

在前面讲解的归并排序(merge sort)中,实际上也是对数组进行进行拆分,然后进行排序,快速排序和归并排序都是分治法的典型应用。 不过不同的是,二者的划分标准不同:

在归并排序中,是将数组不断的划分成两个大小相同的子数组,实际上就是取数组下标的中间值进行拆分,例如{10, 5, 3, 8, 2, 4, 9, 1, 7, 6}划分后的结果为{10, 5, 3, 8, 2}和{4, 9, 1, 7, 6}。

而在快速排序中,数组的划分是基于某一个基准值(pivot)的,拆分时,将所有大于基准值的元素放在一组,将所有小于基准值的元素放在另一组。 比如基准值为5,那么{10, 5, 3, 8, 2, 4, 9, 1, 7, 6}划分后的结果为{1, 5, 3, 4, 2,}和{8, 9, 10, 7, 6}。

需要注意的是,归并排序由于是根据数组下标中间值划分的,因此切分后的两个子数组的长度最多相差1,但是对于快速排序,由于是根据基准值来进行划分,如果基准值选择的不好,例如基准值等于1,那么{10, 5, 3, 8, 2, 4, 9, 1, 7, 6}划分后的结果为{1}和{10, 5, 3, 8, 2, 4, 9, 7, 6},因此在快速排序中,我们需要对划分操作有特别的关注。

划分算法由两个指针来完成,这两个指针分别指向数组的开始和结尾,左指针left pointer向右移动而右指针right pointer向左移动。当left pointer 遇到比基准值小的值时它继续右移,因为这个数据项的位置已经在数组的小于基准值得一边了。当遇到基准值大的数时,它就停下来;当right pointer 遇到比特定值大的数时就继续左移,当遇到比基准值小的数时就停下来。当都停下来的时候left pointer 和right pointer 都指向了在数组错误一方位置上的数据项,所以交换这两个数据项。

交换之后,继续移动两个指针,然后再在合适的实际停止、交换数据,不断重复此过程。当right pointer和left pointer相遇时,划分完成。
这里写图片描述

三数据项取中
关于基准值的选择,理想状态下,应该选择数据项中的中值 作为枢纽,也就是说,应该有一半的数据项大于枢纽,一半的数据项小于枢纽,这会使数组被划分成两个大小相等子数组。为了避免枢纽选择最大或者最小的值,采取了一个折中的方法,找到数组中第一个、最后一个以及中间位置的数据,选择中间位置的数据作为基准值,这种方法称之为”三数据项取中”。

@Test
public void test(){
    int[] arr = new int[] {10, 5, 3, 8, 2, 4, 9, 1, 7, 6};
    quickSort(arr, 0, arr.length - 1);
    System.out.println("quickSort:循环次数" + count1 + ",交换次数:" + count2);
}
private static int count1 = 0;//记录循环次数
private static int count2 = 0;//记录交换次数
/**
 * 划分数组
 */
public static void quickSort(int[] arr, int start, int end) {
    if (start >= end) {
        return;
    }
    int partitionIndex = partition(arr, start, end);
    quickSort(arr, start, partitionIndex);
    quickSort(arr, partitionIndex + 1, end);
}
/**
 * 返回划分后,左指针和右指针相遇的下标
 */
private static int partition(int[] arr, int start, int end) {
    int pivot = getPivot(arr, start, end);
    int left_pointer = start - 1;
    int right_pointer = end + 1;
    while (true) {
        while (arr[++left_pointer] < pivot){
            count1++;
        };// left_pointer当遇到比基准值大的元素,停下来
        while (arr[--right_pointer] > pivot){
            count1++;
        };// right_pointer当遇到比基准值小的元素,停下来
        if (left_pointer >= right_pointer) {
            break;
        }
        swap(arr, left_pointer, right_pointer);
    }
    return right_pointer;
}
/**
 * 获得基准值
 */
private static int getPivot(int[] arr, int start, int end) {
    int median = arr[start];
    if (end - start >= 2) {// 保证有3个数据项
        int left = arr[start];
        int right = arr[end];
        int middle = arr[(start + end) / 2];
        if (left < middle && middle < right) {
            median = middle;
        } else if (left < right && right < middle) {
            median = right;
        } else {
            median = left;
        }
    }
    return median;
}
/**
 * 交换数组中两个元素的位置
 */
private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    count2++;
}

输出结果为:
mergeSort:循环次数20,交换次数:8。

猜你喜欢

转载自blog.csdn.net/zajiayouzai/article/details/79148911
今日推荐