各大排序算法的分析与实现以及时间复杂度
时间复杂度:
时间复杂度是一个算法流程中,常数操作数量的指标。常用O表示。在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项系数,剩下的部分如果记为f(n),那么时间复杂度就是O(f(n))。
一、冒泡排序
思想:n个数一一对比之后找出最大的,再在剩下的n-1个数中一一对比找出第二大的,以此类推。
时间复杂度:O(n^2)
实现代码:
public static void bubbleSort(int[] arr) { for (int i = arr.length - 1; i > 0; i--) { for (int j = 0; j < i; j++) { //如果前一个数比后一个数大就交换 if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); } } } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
二、选择排序
思想:n个数中第一个数与后面的数字比较,若有更小的就交换,从而让第一个数字为最小值,接着再处理剩下的n-1个数,以此类推。
时间复杂度:O(n^2)
实现代码:
public static void insertionSort(int[] arr) { for (int i = 0; i < arr.length; i++) { int minIndex = i; for (int j = i + 1; j < arr.length; j++) { //如果有小于min的值,就改变min的下标 if (arr[j] < arr[minIndex]) { minIndex = j; } } swap(arr, i, minIndex); } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
三、插入排序
思想:第二个数先与第一个数比较,形成有序序列。接着第三个数和前两个形成有序序列的数字比较,找到合适的位置插入。以此类推。
最好的时间复杂度:O(n)
最坏的时间复杂度:O(n^2)
实现代码:
public static void insertionSort(int arr[]) { //从第二个数开始插入 for (int i = 1; i < arr.length; i++) { //该数与前面形成的有序序列进行比较,该数较小的话就交换,直到插入合适位置,较大的话就不交换 for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
四、归并排序
思想:采用分治的思想,将n个元素的数组切成一半,每一半再分别排序,最后将两半已经排序的数组合并。
时间复杂度:O(n*logn)
空间复杂度:O(n)
实现代码:
public static void mergeSort(int[] arr) { if (arr == null || arr.length < 2) return; mergeSort(arr, 0, arr.length - 1); } public static void mergeSort(int[] arr, int index, int end) { if (index == end) return; int mid = index + (end - index) / 2; mergeSort(arr, index, mid); mergeSort(arr, mid + 1, end); //合并两个数组 merge(arr, index, mid, end); } public static void merge(int[] arr, int index, int mid, int end) { //构造辅助数组 int[] help = new int[end - index + 1]; int i = 0; int p1 = index; int p2 = mid + 1; //哪个比较小就放入辅助数组,并且移动指针 while (p1 <= mid && p2 <= end) { help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; } //说明p2指针移动完毕,只剩p1 while (p1 <= mid) { help[i++] = arr[p1++]; } //说明p1指针移动完毕,只剩p2 while (p2 <= end) { help[i++] = arr[p2++]; } //将辅助数组的元素复制回原来数组 for (int j = 0; j < help.length; j++) { arr[index + j] = help[j]; } }
五、快速排序(改进后的)
思想:把数组中的最后一个元素作为分界量,也就是基准点,排成左边比它小的,中间等于它的,右边比它大的。再递归调用分别排左边的和右边的。
最好的时间复杂度:O(n*logn)
最坏的时间复杂度:O(n^2)
长期期望的时间复杂度:O(n*logn)
长期期望的额外空间复杂度:O(logn)
实现代码:
public static void quickSort(int[] arr) { if (arr == null || arr.length < 2) return; quickSort(arr, 0, arr.length - 1); } public static void quickSort(int[] arr, int index, int end) { if (index < end) { //swap(arr, l + (int) (Math.random() * (r - l + 1)), r);(这一句加上就是随机快速排序,暂时不加) //p数组代表的是等于划分基准点的元素的初始和结束位置 int[] p = partition(arr, index, end); quickSort(arr, index, p[0] - 1); quickSort(arr, p[1] + 1, end); } } //下面是以数组的最后一个元素作为基准点的,这个方法完成的结果就是小于基准点放左边,等于基准点放中间,大于基准点放右边 public static int[] partition(int[] arr, int index, int end) { //less指针用来划分小于基准点的范围 int less = index - 1; //more指针用来划分大于基准点的范围 int more = end; while (index < more) { //如果当前元素小于基准点的元素,就让它与less指针的前一个数字交换,并移动less指针,移动index指针操作下一个数 //如果当前元素大于基准点的元素,就让它与more指针的前一个数字交换,并移动more指针 //如果当前元素等于基准点的元素,移动index指针操作下一个数 if (arr[index] < arr[end]) { swap(arr, index++, ++less); } else if (arr[index] > arr[end]) { swap(arr, index, --more); } else { index++; } } //由于基准点的元素一直没有动过,最后要让它和大于基准点的元素交换 swap(arr, more, end); //返回等于基准点元素的下标 return new int[]{less + 1, more}; } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
六、堆排序
思想:先让数组的n个元素依次进堆,构建出大顶堆。然后交换顶堆的元素和最末尾的元素(最末尾的元素不一定最小,但是一定小于等于堆顶的元素),然后让剩下的n-1元素进行调整,重新调整为大顶堆。然后在进行交换,再调整,以此类推。
时间复杂度:O(n*logn)
实现代码:
public static void heapSort(int[] arr) { if (arr == null || arr.length < 2) return; for (int i = 0; i < arr.length; i++) { heapInsert(arr, i); } int size = arr.length; swap(arr, 0, --size); while (size > 0) { heapify(arr, 0, size); swap(arr, 0, --size); } } //建立大顶堆的过程 public static void heapInsert(int[] arr, int index) { //如果当前值大于父节点的话,与父节点交换 while (arr[index] > arr[(index - 1) / 2]) { swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } } //重新调整成为大顶堆的过程 public static void heapify(int[] arr, int index, int size) { //取该结点的左结点 int left = index * 2 + 1; //如果左结点在size范围内 while (left < size) { //从左右结点中选出最大的,赋给largest int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left; //判断当前结点和左右结点的最大值比较,如果当前结点大就维持原样(因为调整到index的结点可能比左结点的值要大的) if (arr[largest]<arr[index]) { break; } //交换index和largest结点的值 swap(arr, largest, index); //让index指针移动到largest结点位置 index = largest; //取index的左结点 left = index * 2 + 1; } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
排序算法的稳定性
排完顺序之后不会改变相同的原数字在原来数组中的相对次序,称为稳定。反之为不稳定
冒泡排序:稳定
选择排序:不稳定
插入排序:稳定
归并排序:稳定
快速排序:不稳定
堆排序:不稳定