基本思想前端
给定一个数组,咱们把数组里的元素统统倒入到水池中,这些元素将经过相互之间的比较,按照大小顺序一个一个地像气泡同样浮出水面。java
实现算法
每一轮,从杂乱无章的数组头部开始,每两个元素比较大小并进行交换,直到这一轮当中最大或最小的元素被放置在数组的尾部,而后不断地重复这个过程,直到全部元素都排好位置。其中,核心操做就是元素相互比较。后端
例题分析数组
给定数组 [2, 1, 7, 9, 5, 8],要求按照从左到右、从小到大的顺序进行排序。数据结构
解题思路dom
从左到右依次冒泡,把较大的数往右边挪动便可。函数
首先指针指向第一个数,比较第一个数和第二个数的大小,因为 2 比 1 大,因此两两交换,[1, 2, 7, 9, 5, 8]。学习
接下来指针往前移动一步,比较 2 和 7,因为 2 比 7 小,二者保持不动,[1, 2, 7, 9, 5, 8]。到目前为止,7 是最大的那个数。优化
指针继续往前移动,比较 7 和 9,因为 7 比 9 小,二者保持不动,[1, 2, 7, 9, 5, 8]。如今,9 变成了最大的那个数。
再日后,比较 9 和 5,很明显,9 比 5 大,交换它们的位置,[1, 2, 7, 5, 9, 8]。
最后,比较 9 和 8,9 比 8 大,交换它们的位置,[1, 2, 7, 5, 8, 9]。通过第一轮的两两比较,9 这个最大的数就像冒泡同样冒到了数组的最后面。
接下来进行第二轮的比较,把指针从新指向第一个元素,重复上面的操做,最后,数组变成了:[1, 2, 5, 7, 8, 9]。
在进行新一轮的比较中,判断一下在上一轮比较的过程当中有没有发生两两交换,若是一次交换都没有发生,就证实其实数组已经排好序了。
实现代码
public static void bubbleSort(int[] nums) { // 定义一个布尔变量 hasChange,用来标记每轮遍历中是否发生了交换 boolean hasChange = true; for (int i = 0; i < nums.length - 1 && hasChange; i++) { // 每轮遍历开始,将 hasChange 设置为 false hasChange = false; // 进行两两比较,若是发现当前的数比下一个数还大,那么就交换这两个数,同时记录一下有交换发生 for (int j = 0; j < nums.length - 1 - i; j++) { if (nums[j] > nums[j+1]) { swap(nums, j, j+1); hasChange = true; } } } } // 交换数组中的两个数 public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
算法分析
空间复杂度
假设数组的元素个数是 n,因为在整个排序的过程当中,咱们是直接在给定的数组里面进行元素的两两交换,因此空间复杂度是 O(1)。
时间复杂度
给定的数组按照顺序已经排好
在这种状况下,咱们只须要进行 n−1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的状况。
给定的数组按照逆序排列
在这种状况下,咱们须要进行 n(n-1)/2 次比较,时间复杂度是 O(n2)。这是最坏的状况。
给定的数组杂乱无章
在这种状况下,平均时间复杂度是 O(n2)。
因而可知,冒泡排序的时间复杂度是 O(n2)。它是一种稳定的排序算法。(稳定是指若是数组里两个相等的数,那么排序先后这两个相等的数的相对位置保持不变。)
基本思想
不断地将还没有排好序的数插入到已经排好序的部分。
特色
在冒泡排序中,通过每一轮的排序处理后,数组后端的数是排好序的;而对于插入排序来讲,通过每一轮的排序处理后,数组前端的数都是排好序的。
例题分析
对数组 [2, 1, 7, 9, 5, 8] 进行插入排序。
解题思路
首先将数组分红左右两个部分,左边是已经排好序的部分,右边是尚未排好序的部分,刚开始,左边已排好序的部分只有第一个元素 2。接下来,咱们对右边的元素一个一个进行处理,将它们放到左边。
实现代码
public static void insertionSort(int[] nums) { // 将数组的第一个元素看成已经排好序的,从第二个元素,即 i 从 1 开始遍历数组 for (int i = 1, j, current; i < nums.length; i++) { // 外围循环开始,把当前 i 指向的值用 current 保存 current = nums[i]; // 指针 j 内循环,和 current 值比较,若 j 所指向的值比 current 值大,则该数右移一位 for (j = i - 1; j >= 0 && nums[j] > current; j--) { nums[j + 1] = nums[j]; } // 内循环结束,j+1 所指向的位置就是 current 值插入的位置 nums[j + 1] = current; } }
算法分析
空间复杂度
假设数组的元素个数是 n,因为在整个排序的过程当中,是直接在给定的数组里面进行元素的两两交换,空间复杂度是 O(1)。
时间复杂度
给定的数组按照顺序已经排好
只须要进行 n-1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的状况。
给定的数组按照逆序排列
在这种状况下,咱们须要进行 n(n-1)/2 次比较,时间复杂度是 O(n2)。这是最坏的状况。
给定的数组杂乱无章
在这种状况下,平均时间复杂度是 O(n2)。
因而可知,和冒泡排序同样,插入排序的时间复杂度是 O(n2),而且它也是一种稳定的排序算法。
基本思想
核心是分治,就是把一个复杂的问题分红两个或多个相同或类似的子问题,而后把子问题分红更小的子问题,直到子问题能够简单的直接求解,最原问题的解就是子问题解的合并。归并排序将分治的思想体现得淋漓尽致。
实现
一开始先把数组从中间划分红两个子数组,一直递归地把子数组划分红更小的子数组,直到子数组里面只有一个元素,才开始排序。
排序的方法就是按照大小顺序合并两个元素,接着依次按照递归的返回顺序,不断地合并排好序的子数组,直到最后把整个数组的顺序排好。
例题分析
例题:利用归并排序算法对数组 [2, 1, 7, 9, 5, 8] 进行排序。
解题思路
首先不断地对数组进行切分,直到各个子数组里只包含一个元素。
接下来递归地按照大小顺序合并切分开的子数组,递归的顺序和二叉树里的前序遍历相似。
合并数组 [1, 2, 7] 和 [5, 8, 9] 的操做步骤以下。
合并之因此能成功,先决条件必须是两个子数组都已经分别排好序了。
实现代码
public static void mergeSort(int[] arr, int lo, int hi) { // 判断是否只剩下最后一个元素 if (lo >= hi) { return; } // 从中间将数组分红两个部分 int mid = lo + (hi - lo) / 2; // 分别递归地将左右两半排好序 mergeSort(arr, lo, mid); mergeSort(arr, mid + 1, hi); // 将排好序的左右两半合并 merge(arr, lo, mid, hi); } // 归并 public static void merge(int[] nums, int lo, int mid, int hi) { // 复制一份原来的数组 int[] copy = nums.clone(); // 定义一个 k 指针表示从什么位置开始修改原来的数组,i 指针表示左半边的起始位置,j 表示右半边的起始位置 int k = lo, i = lo, j = mid + 1; while(k <= hi) { if(i > mid) { nums[k++] = copy[j++]; } else if(j > hi) { nums[k++] = copy[i++]; } else if(copy[j] < copy[i]) { nums[k++] = copy[j++]; } else { nums[k++] = copy[i++]; } } }
其中,While 语句比较,一共可能会出现四种状况。
算法分析
空间复杂度
因为合并 n 个元素须要分配一个大小为 n 的额外数组,合并完成以后,这个数组的空间就会被释放,因此算法的空间复杂度就是 O(n)。归并排序也是稳定的排序算法。
时间复杂度
归并算法是一个不断递归的过程。
举例:数组的元素个数是 n,时间复杂度是 T(n) 的函数。
解法:把这个规模为 n 的问题分红两个规模分别为 n/2 的子问题,每一个子问题的时间复杂度就是 T(n/2),那么两个子问题的复杂度就是 2×T(n/2)。当两个子问题都获得了解决,即两个子数组都排好了序,须要将它们合并,一共有 n 个元素,每次都要进行最多 n-1 次的比较,因此合并的复杂度是 O(n)。由此咱们获得了递归复杂度公式:T(n) = 2×T(n/2) + O(n)。
对于公式求解,不断地把一个规模为 n 的问题分解成规模为 n/2 的问题,一直分解到规模大小为 1。若是 n 等于 2,只须要分一次;若是 n 等于 4,须要分 2 次。这里的次数是按照规模大小的变化分类的。
以此类推,对于规模为 n 的问题,一共要进行 log(n) 层的大小切分。在每一层里,咱们都要进行合并,所涉及到的元素其实就是数组里的全部元素,所以,每一层的合并复杂度都是 O(n),因此总体的复杂度就是 O(nlogn)。
基本思想
快速排序也采用了分治的思想。
实现
把原始的数组筛选成较小和较大的两个子数组,而后递归地排序两个子数组。
举例:把班里的全部同窗按照高矮顺序排成一排。
解法:老师先随机地挑选了同窗 A,让全部其余同窗和 A 比高矮,比 A 矮的都站在 A 的左边,比 A 高的都站在 A 的右边。接下来,老师分别从左边和右边的同窗里选择了同窗 B 和 C,而后不断地筛选和排列下去。
在分红较小和较大的两个子数组过程当中,如何选定一个基准值(也就是同窗 A、B、C 等)尤其关键。
例题分析
对数组 [2, 1, 7, 9, 5, 8] 进行排序。
解题思路
实现代码
public static void quickSort(int[] nums, int lo, int hi) { // 判断是否只剩下一个元素,是则直接返回 if (lo >= hi) { return; } // 利用partition函数找到一个随机基准点 int p = partition(nums, lo, hi); // 递归地对基准点左半边和右半边的数进行排序 quickSort(nums, lo, p - 1); quickSort(nums, p + 1, hi); } // 得到基准值 public static int partition(int[] nums, int lo, int hi) { // 随机选择一个数做为基准值,nums[hi] 就是基准值 swap(nums, randRange(lo, hi), hi); int i, j; // 从左到右用每一个数和基准值比较,若比基准值小,则放到指针 i 所指向的位置。循环完毕后,i 指针以前的数都比基准值小 for (i = lo, j = lo; j < hi; j++) { if (nums[j] <= nums[hi]) { swap(nums, i++, j); } } // 末尾的基准值放置到指针 i 的位置,i 指针以后的数都比基准值大 swap(nums, i, j); // 返回指针 i,做为基准点的位置 return i; } // 获取随机值 public static int randRange(int lo, int hi) { return (int) (lo + Math.random() * (hi - lo + 1)); } // 交换数组中的两个数 public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
算法分析
空间复杂度
和归并排序不一样,快速排序在每次递归的过程当中,只须要开辟 O(1) 的存储空间来完成交换操做实现直接对数组的修改,又由于递归次数为 logn,因此它的总体空间复杂度彻底取决于压堆栈的次数,所以它的空间复杂度是 O(logn)。
时间复杂度
最优状况:被选出来的基准值都是当前子数组的中间数。
这样的分割,能保证对于一个规模大小为 n 的问题,能被均匀分解成两个规模大小为 n/2 的子问题(归并排序也采用了相同的划分方法),时间复杂度就是:T(n) = 2×T(n/2) + O(n)。
把规模大小为 n 的问题分解成 n/2 的两个子问题时,和基准值进行了 n-1 次比较,复杂度就是 O(n)。很显然,在最优状况下,快速排序的复杂度也是 O(nlogn)。
最坏状况:基准值选择了子数组里的最大或者最小值
每次都把子数组分红了两个更小的子数组,其中一个的长度为 1,另一个的长度只比原子数组少 1。划分过程和冒泡排序的过程相似,算法复杂度为 O(n2)。
tips:能够经过随机地选取基准值来避免出现最坏的状况。
基本思想
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似彻底二叉树的结构,并同时知足堆的性质:即子结点的键值或索引老是小于(或者大于)它的父节点。
实现
实现代码
public static void heapSort(int[] arr) { len = arr.length; if (len < 1) { return; } //1.构建一个大根堆 buildMaxHeap(arr); //2.循环将堆首位(最大值)与末位交换,而后再从新调整最大堆 while(len > 0) { swap(arr, 0, len-1); len--; adjustHeap(arr, 0); } } // 创建大根堆 public static void buildMaxHeap(int[] arr) { // 从最后一个非叶子节点开始向上构造大根堆 for (int i = (len / 2 -1); i >= 0; i--) { adjustHeap(arr, i); } } // 调整使之成为大根堆 public static void adjustHeap(int[] arr, int i ) { int maxIndex = i; // 若是有左子树且左子树大于父节点,则将最大指针指向左子树 if (i * 2 < len && arr[i * 2] > arr[maxIndex]) { maxIndex = i * 2; } // 若是有右子树且右子树大于父节点,则将最大指针指向右子树 if (i * 2 + 1 < len && arr[i * 2 + 1] > arr[maxIndex]) { maxIndex = i * 2 + 1; } // 若是父节点不是最大值,则将父节点与最大值交换并递归调整与父节点交换的位置 if (maxIndex != i) { swap(arr, maxIndex, i); adjustHeap(arr, maxIndex); } } // 交换数组中的两个数 public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
算法分析
时间复杂度
堆排序是一种选择排序,总体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程当中,需交换n-1次,而重建堆的过程当中,根据彻底二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。因此堆排序时间复杂度通常认为就是O(nlogn)级。
基本思想
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。做为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有肯定范围的整数。
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。而后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。
实现
动图演示
实现代码
public static void countingSort(int[] arr) { if (arr.length == 0) { return; } int bias, min = arr[0], max = arr[0]; // 1.确认数组中的最大值最小值 for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } if (arr[i] < min) { min = arr[i]; } } bias = 0 - min; // bias记录新数组的下标偏移量 int[] bucket = new int[max - min + 1]; // 2.统计并存入新数组 for (int i = 0; i < arr.length; i++) { bucket[arr[i] + bias]++; } int index = 0, i = 0; // 3.反向填充目标数组 while(index < arr.length) { if (bucket[i] != 0) { arr[index] = i - bias; bucket[i]--; index++; } else { i++; } } }
算法分析
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。因为用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,须要大量时间和内存。
最佳状况:T(n) = O(n+k) 最差状况:T(n) = O(n+k) 平均状况:T(n) = O(n+k)
思想
桶排序
是计数排序的升级版。当数列取值范围过大,或者不是整数时不能适用计数排序,这时可使用桶排序来解决问题。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的肯定。
桶排序 (Bucket sort)
的工做的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每一个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
实现
每个桶(bucket)表明一个区间范围,里面能够承载一个或多个元素。桶排序的第一步,就是建立这些桶,肯定每个桶的区间范围:
具体创建多少个桶,如何肯定桶的区间范围,有不少不一样的方式。咱们这里建立的桶数量等于原始数列的元素数量,除了最后一个桶只包含数列最大值,前面各个桶的区间按照比例肯定。
区间跨度 = (最大值-最小值)/ (桶的数量 - 1)
第二步,遍历原始数列,把元素对号入座放入各个桶中:
第三步,每一个桶内部的元素分别排序(显然,只有第一个桶须要排序):
第四步,遍历全部的桶,输出全部元素:
0.5,0.84,2.18,3.25,4.5
到此为止,排序结束。
实现代码
public static void bucketSort(double[] array){ //1.获得数列的最大值和最小值,并算出差值d double max = array[0]; double min = array[0]; for(int i=1; i<array.length; i++) { if(array[i] > max) { max = array[i]; } if(array[i] < min) { min = array[i]; } } double d = max - min; //2.初始化桶 int bucketNum = array.length; ArrayList<LinkedList<Double>> bucketList = new ArrayList<LinkedList<Double>>(bucketNum); for(int i = 0; i < bucketNum; i++){ bucketList.add(new LinkedList<Double>()); } //3.遍历原始数组,将每一个元素放入桶中 for(int i = 0; i < array.length; i++){ int num = (int)((array[i] - min) * (bucketNum-1) / d); bucketList.get(num).add(array[i]); } //4.对每一个通内部进行排序 for(int i = 0; i < bucketList.size(); i++){ //JDK底层采用了归并排序或归并的优化版本 Collections.sort(bucketList.get(i)); } //5.输出所有元素 int index = 0; for(LinkedList<Double> list : bucketList){ for(double element : list){ array[index] = element; index++; } } }
算法分析
时间复杂度:O(N + C)
对于待排序序列大小为 N,共分为 M 个桶,主要步骤有:
通常使用较为快速的排序算法,时间复杂度为 O ( N l o g N ),实际的桶排序过程是以链表形式插入的。
整个桶排序的时间复杂度为:
O ( N ) + O ( M ∗ ( N / M ∗ l o g ( N / M ) ) ) = O ( N ∗ ( l o g ( N / M ) + 1 ) )
当 N = M 时,复杂度为 O ( N )
空间复杂度:O(N+M)
基本思想
和前面介绍的几种排序不一样,拓扑排序应用的场合再也不是一个简单的数组,而是研究图论里面顶点和顶点连线之间的性质。拓扑排序就是要将这些顶点按照相连的性质进行排序。
要能实现拓扑排序,得有几个前提:
拓扑排序通常用来理清具备依赖关系的任务。
举例:假设有三门课程 A、B、C,若是想要学习课程 C 就必须先把课程 B 学完,要学习课程 B还得先学习课程 A,因此得出课程的学习顺序应该是 A -> B -> C。
实现
例题分析
有一个学生想要修完 5 门课程的学分,这 5 门课程分别用 一、二、三、四、5 来表示,如今已知学习这些课程有以下的要求:
课程 2 和 4 依赖于课程 1
课程 3 依赖于课程 2 和 4
课程 4 依赖于课程 1 和 2
课程 5 依赖于课程 3 和 4
那么这个学生应该按照怎样的顺序来学习这 5 门课程呢?
解题思路
能够把 5 门课程当作是一个图里的 5 个顶点,用有向线段按照它们的相互关系连起来,因而得出下面的有向图。
首先能够看到,这个有向图里没有环,不管从哪一个顶点出发,都不会再回到那个顶点。而且,这个图里并无孤岛的出现,所以,咱们能够对它进行拓扑排序。
方法就是,一开始的时候,对每一个顶点统计它们各自的前驱(也就是入度):1(0),2(1),3(2),4(1),5(2)。
通常来讲,一个有向无环图能够有一个或多个拓扑排序的序列。
实现代码
运用广度优先搜索的方法对这个图的结构进行遍历。在构建这个图的过程当中,用一个连接矩阵 adj 来表示这个图的结构,用一个 indegree 的数组统计每一个顶点的入度,重点看如何实现拓扑排序。
void topologicalSort() { Queue<Integer> q = new LinkedList(); // 定义一个队列 q // 将全部入度为 0 的顶点加入到队列 q for (int v = 0; v < V; v++) { if (indegree[v] == 0) q.add(v); } // 循环,直到队列为空 while (!q.isEmpty()) { int v = q.poll(); // 每次循环中,从队列中取出顶点,即为按照入度数目排序中最小的那个顶点 print(v); // 将跟这个顶点相连的其余顶点的入度减 1,若是发现那个顶点的入度变成了 0,将其加入到队列的末尾 for (int u = 0; u < adj[v].length; u++) { if (--indegree[u] == 0) { q.add(u); } } } }
算法分析
时间复杂度
统计顶点的入度须要 O(n) 的时间,接下来每一个顶点被遍历一次,一样须要 O(n) 的时间,因此拓扑排序的时间复杂度是 O(n)。