网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
从数组中第⼀个数开始,依次遍历数组中的每⼀个数,通过相邻⽐较交换,每⼀轮循环下来找出剩余未排序数的中的最⼤数并”冒泡”⾄数列的顶端。
算法步骤:
- 从数组中第⼀个数开始,依次与下⼀个数⽐较并次交换⽐⾃⼰⼩的数,直到最后⼀个数。如果发⽣交换,则继续下⾯的步骤,如果未
发⽣交换,则数组有序,排序结束,此时时间复杂度为O(n);- 每⼀轮”冒泡”结束后,最⼤的数将出现在乱序数列的最后⼀位。重复步骤(1)。
稳定性:稳定排序
复杂度:
时间复杂度: O(n)⾄O(n2),平均时间复杂度为O(n2)。
最好的情况:如果待排序数据序列为正序,则⼀趟冒泡就可完成排序,排序码的⽐较次数为n-1次,且没有移动,时间复杂度为O(n)。
最坏的情况:如果待排序数据序列为逆序,则冒泡排序需要n-1次趟起泡,每趟进⾏n-i次排序码的⽐较和移动,即⽐较和移动次数均达到最⼤值:
⽐较次数:Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n2)
移动次数等于⽐较次数,因此最坏时间复杂度为O(n2)。
⽰例代码:
public class BubleSort {
public static void main(String[] args) {
int[] arr = { 1, 3, 2, 5, 4, 6, 100, 20 };
bubbleSort(arr);
for (int i : arr) {
System.out.print(i + " ");
}
}
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
// 外层循环控制遍历次数
for (int j = 0; j < arr.length - 1 - i; j++) {
// 内层循环控制每次遍历比较的位置
if (arr[j] > arr[j + 1]) {
// 如果相邻元素顺序错误,交换它们的位置
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
具体的过程可以这样解释:
数组中有n个元素,第一次遍历从左到右,比较相邻的元素,如果左边的元素大于右边的元素,就交换它们的位置。这样每次遍历都会将最大的元素排在数组的右侧。第二次遍历从左到右,除了最后一个元素,每个元素都和后面的元素比较,依次类推,直到数组被完全排好序。
快速排序
冒泡排序是在相邻的两个记录进⾏⽐较和交换,每次交换只能上移或下移⼀个位置,导致总的⽐较与移动次数较多。快速排序⼜称分区交换排序,是对冒泡排序的改进,快速排序采⽤的思想是分治思想。
算法原理:
- 从待排序的n个记录中任意选取⼀个记录(通常选取第⼀个记录)为分区标准;
- 把所有⼩于该排序列的记录移动到左边,把所有⼤于该排序码的记录移动到右边,中间放所选记录,称之为第⼀趟排序;
- 然后对前后两个⼦序列分别重复上述过程,直到所有记录都排好序。
稳定性:不稳定排序
复杂度:
时间复杂度: O(nlog2n)⾄O(n2),平均时间复杂度为O(nlgn)。
最好的情况:是每趟排序结束后,每次划分使两个⼦⽂件的长度⼤致相等,时间复杂度为O(nlog2n)。
最坏的情况:是待排序记录已经排好序,第⼀趟经过n-1次⽐较后第⼀个记录保持位置不变,并得到⼀个n-1个元素的⼦记录;第⼆趟经过
n-2次⽐较,将第⼆个记录定位在原来的位置上,并得到⼀个包括n-2个记录的⼦⽂件,依次类推,这样总的⽐较次数是:
Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n2)
⽰例代码:
public class QuickSort {
public static void main(String[] args) {
int arr[]={3,1,23,3,1,4,5,199,20};
new QuickSort().quickSort(arr, 0, arr.length-1);
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
public void quickSort(int[] arr, int start, int end) {
if (start < end) {
int pivot = partition(arr, start, end);
quickSort(arr, start, pivot - 1);
quickSort(arr, pivot + 1, end);
}
}
public int partition(int[] arr, int start, int end) {
int pivot = arr[start];
int left = start + 1;
int right = end;
while (left <= right) {
while (left <= right && arr[left] < pivot) {
left++;
}
while (left <= right && arr[right] >= pivot) {
right--;
}
if (left < right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}
int temp = arr[start];
arr[start] = arr[right];
arr[right] = temp;
return right;
}
}
插⼊类排序
插⼊排序的基本⽅法是:每步将⼀个待排序的记录,按其排序码⼤⼩,插到前⾯已经排序的⽂件中的适当位置,直到全部插⼊完为⽌。
直接插入排序
原理:
从待排序的n个记录中的第⼆个记录开始,依次与前⾯的记录⽐较并寻找插⼊的位置,每次外循环结束后,将当前的数插⼊到合适的位置。
稳定性:稳定排序
复杂度:
时间复杂度: O(n)⾄O(n2),平均时间复杂度是O(n2)。
最好情况:当待排序记录已经有序,这时需要⽐较的次数是Cmin=n−1=O(n)。
最坏情况:如果待排序记录为逆序,则最多的⽐较次数为Cmax=∑i=1n−1(i)=n(n−1)2=O(n2)。
⽰例代码:
public class InsertSort {
public static void main(String[] args) {
int[] arr = { 1, 3, 2, 5, 4, 6, 100, 20 };
insertionSort(arr);
for (int i : arr) {
System.out.print(i + " ");
}
}
public static void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++) {
int j = i - 1;
int temp = arr[i];
while (j >= 0 && arr[j] > temp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = temp;
}
}
}
说明:
直接插入排序的基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。在代码中,我们假设数组的第一个元素已经是有序表,从第二个元素开始遍历。对于第i个元素,我们将其暂存到一个临时变量中,然后从i-1开始向前遍历,将所有比它大的元素都向后移动一个位置,直到找到第一个比它小的元素,将其插入到这个位置。重复这个过程,直到整个数组排好序为止。
Shell排序
Shell 排序⼜称缩⼩增量排序, 由D. L. Shell在1959年提出,是对直接插⼊排序的改进。
原理: Shell排序法是对相邻指定距离(称为增量)的元素进⾏⽐较,并不断把增量缩⼩⾄1,完成排序。
Shell排序开始时增量较⼤,分组较多,每组的记录数⽬较少,故在各组内采⽤直接插⼊排序较快,后来增量di逐渐缩⼩,分组数减少,各组的记录数增多,但由于已经按di−1分组排序,⽂件叫接近于有序状态,所以新的⼀趟排序过程较快。因此Shell排序在效率上⽐直接插⼊排序有较⼤的改进。
在直接插⼊排序的基础上,将直接插⼊排序中的1全部改变成增量d即可,因为Shell排序最后⼀轮的增量d就为1。
稳定性:不稳定排序。
复杂度:
时间复杂度:O(n1.3)到O(n2)。Shell排序算法的时间复杂度分析⽐较复杂,实际所需的时间取决于各次排序时增量的个数和增量的取值。
研究证明,若增量的取值⽐较合理,Shell排序算法的时间复杂度约为O(n1.3)。
对于增量的选择,Shell 最初建议增量选择为n/2,并且对增量取半直到 1;D. Knuth教授建议di+1=⌊di−13⌋序列。
注意:增量每次变化取前⼀次增量的⼀般,当增量d等于1时,shell排序就退化成了直接插⼊排序了。
示例代码:
public class ShellSort {
public static void main(String[] args) {
int arr[] = { 3, 1, 23, 3, 1, 4, 5, 199, 20 };
shellSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void shellSort(int[] arr) {
int n = arr.length;
for (int gap = n / 2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
选择类排序
选择类排序的基本⽅法是:每步从待排序记录中选出排序码最⼩的记录,顺序放在已排序的记录序列的后⾯,知道全部排完。
简单选择排序(⼜称直接选择排序)
原理:
从所有记录中选出最⼩的⼀个数据元素与第⼀个位置的记录交换;然后在剩下的记录当中再找最⼩的与第⼆个位置的记录交换,循环到只剩下最后⼀个数据元素为⽌。
稳定性:不稳定排序
时间复杂度:
最坏、最好和平均复杂度均为O(n2),因此,简单选择排序也是常见排序算法中性能最差的排序算法。简单选择排序的⽐较次数与⽂件的初始状态没有关系,在第i趟排序中选出最⼩排序码的记录,需要做n-i次⽐较,因此总的⽐较次数是:
∑i=1n−1(n−i)=n(n−1)/2=O(n2)。
⽰例代码:
public static void selectionSort(int[] arr){
int n = arr.length;
for(int i = 0; i < n - 1; 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;
}
}
基本思想:
对于n个元素的序列,首先在其中找到最小的元素,然后将其放到序列的最前面;接着,再从剩余的n-1个元素中找到最小的元素,放到已排序的序列的末尾;重复这个过程,直到整个序列排好序为止。在代码中,我们通过两个嵌套的循环实现了这个过程。外层循环从第一个元素开始遍历到倒数第二个元素,内层循环从外层循环的下一个元素开始遍历到最后一个元素,找到其中最小的元素,并记录其下标minIndex。在内层循环结束后,如果minIndex不等于外层循环的下标i,说明找到了更小的元素,将其与arr[i]交换即可。重复这个过程,直到整个数组排好序为止。
堆排序
直接选择排序中,第⼀次选择经过了n-1次⽐较,只是从排序码序列中选出了⼀个最⼩的排序码,⽽没有保存其他中间⽐较结果。所以后⼀趟排序时⼜要重复许多⽐较操作,降低了效率。J. Willioms和Floyd在1964年提出了堆排序⽅法,避免这⼀缺点。
原理:
堆排序是一种基于二叉堆的排序算法,它的基本思想是将待排序的序列构建成一个大根堆(或小根堆),然后将堆顶元素与堆底元素交换位置,然后重新调整堆,将剩下的元素构建成一个新的大根堆(或小根堆),重复这个过程,直到整个序列排好序为止。在代码中,我们首先通过adjustHeap方法将待排序序列构建成一个大根堆,然后将堆顶元素与堆底元素交换位置,再将剩下的元素构建成一个新的大根堆,重复这个过程,直到整个序列排好序为止。
稳定性: 不稳定排序
在堆排序过程中,由于堆的构建和调整过程中都需要进行元素交换,因此可能会导致相同元素之间的相对位置发生改变。例如,对于序列[3, 3’, 2],如果先将其构建成大根堆,就会得到[3’, 3, 2],然后将堆顶元素3’与堆底元素2交换位置,得到[2, 3, 3’],此时3和3’的相对位置就发生了改变。因此,堆排序是一种不稳定的排序算法。
复杂度:
堆排序的时间复杂度为O(nlogn),其中n是待排序序列的长度。堆排序的基本过程可以分为两个步骤:构建堆和堆排序。构建堆的时间复杂度为O(n),堆排序的时间复杂度为O(nlogn)。因此,堆排序的总时间复杂度为O(n+nlogn)=O(nlogn)。
堆排序的空间复杂度为O(1),它不需要额外的存储空间来存储中间结果,只需要在原始数组上进行原地排序。因此,堆排序是一种空间复杂度为O(1)的原地排序算法。
堆排序的时间复杂度虽然不如快速排序和归并排序那么优秀,但是它具有稳定的时间复杂度,不受输入数据的影响。同时,堆排序是一种原地排序算法,不需要额外的存储空间,因此它在一些内存受限的场景下具有优势。
示例代码:
public class HeapSort {
public static void main(String[] args) {
int arr[] = { 3, 1, 23, 3, 1, 4, 5, 199, 20 };
heapSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void heapSort(int[] arr){
int n = arr.length;
// 构建大根堆
for(int i = n / 2 - 1; i >= 0; i--){
adjustHeap(arr, i, n);
}
// 进行排序
for(int i = n - 1; i > 0; i--){
swap(arr, 0, i);
adjustHeap(arr, 0, i);
}
}
private static void adjustHeap(int[] arr, int i, int n){
int temp = arr[i];
for(int j = 2 \* i + 1; j < n; j = 2 \* j + 1){
if(j + 1 < n && arr[j + 1] > arr[j]){
j++;
}
if(arr[j] > temp){
arr[i] = arr[j];
i = j;
}else{
break;
}
}
arr[i] = temp;
}
private static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
归并排序
二路归并排序
算法思想:
归并排序属于⽐较类⾮线性时间排序,号称⽐较类排序中性能最佳者,在数据中应⽤中较⼴。
归并排序是分治法(Divide and Conquer)的⼀个典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,
再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。
稳定性:稳定排序算法;
时间复杂度: 最坏,最好和平均时间复杂度都是Θ(nlgn)。
示例代码:
public class MergeSort {
public static void main(String[] args) {
int arr[] = { 3, 1, 23, 3, 1, 4, 5, 199, 20 };
mergeSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void mergeSort(int[] arr){
int n = arr.length;
int[] temp = new int[n];
sort(arr, 0, n - 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, j = mid + 1, k = 0;
while(i <= mid && j <= right){
if(arr[i] <= arr[j]){
temp[k++] = arr[i++];
}else{
temp[k++] = arr[j++];
}
}
while(i <= mid){
temp[k++] = arr[i++];
}
while(j <= right){
temp[k++] = arr[j++];
}
for(int p = 0; p < k; p++){
arr[left + p] = temp[p];
}
}
}
算法思想:
归并排序的基本思想是:将待排序序列分成两个子序列,对这两个子序列分别进行归并排序,然后将它们合并成一个有序序列。在代码中,我们通过sort方法递归地将待排序序列分成两个子序列,然后对这两个子序列分别进行归并排序。在归并排序的过程中,我们需要用到一个临时数组temp来存储归并结果。我们通过merge方法将两个有序子序列合并成一个有序序列。具体地,我们使用三个指针i、j和k来遍历两个有序子序列和临时数组temp,比较arr[i]和arr[j]的大小,将较小的元素存入temp[k]中,并将对应的指针向后移动一位。当其中一个子序列遍历完时,我们将另一个子序列剩余的元素全部存入temp中。最后,我们将temp中的元素复制回原始数组arr中,完成归并排序的过程。
多路排序
示例代码:
public class MultMergeSort {
public static void main(String[] args) {
int arr[] = { 3, 1, 23, 3, 1, 4, 5, 199, 20 };
mergeSort(arr,3);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void mergeSort(int[] arr, int m){
int n = arr.length;
int[] temp = new int[n];
sort(arr, 0, n - 1, temp, m);
}
private static void sort(int[] arr, int left, int right, int[] temp, int m){
if(left < right){
int len = (right - left + 1) / m;
for(int i = 0; i < m; i++){
int l = left + i \* len;
int r = l + len - 1;
if(i == m - 1){
r = right;
}
sort(arr, l, r, temp, m);
}
merge(arr, left, right, temp, m);
}
}
private static void merge(int[] arr, int left, int right, int[] temp, int m){
int[] p = new int[m];
int len = (right - left + 1) / m;
for(int i = 0; i < m; i++){
p[i] = left + i \* len;
}
int k = left;
while(k <= right){
int min = Integer.MAX_VALUE;
int idx = -1;
for(int i = 0; i < m; i++){
if(p[i] <= left + i \* len + len - 1 && arr[p[i]] < min){
min = arr[p[i]];
idx = i;
}
}
if(idx == -1){
break;
}
temp[k++] = arr[p[idx]++];
}
for(int i = left; i <= right; i++){
arr[i] = temp[i];
}
}
}
算法基本思想:
多路归并排序是归并排序的一种变种,它将待排序序列分成多个子序列,对每个子序列分别进行排序,然后将它们合并成一个有序序列。在代码中,我们通过sort方法递归地将待排序序列分成多个子序列,然后对每个子序列分别进行归并排序。在归并排序的过程中,我们需要用到一个临时数组temp来存储归并结果。我们通过merge方法将多个有序子序列合并成一个有序序列。具体地,我们首先将待排序序列分成m个子序列,对每个子序列分别进行归并排序。然后,我们通过p数组来记录每个子序列的当前位置,使用一个循环来遍历所有的子序列,每次找到当前m个子序列中最小的元素,将其存入temp中,并将对应的指针向后移动一位。当所有子序列遍历完时,我们将temp中的元素复制回原始数组arr中,完成归并排序的过程。
线性时间非比较类排序
计数排序
计数排序是⼀个⾮基于⽐较的排序算法,该算法于1954年由 Harold H. Seward 提出,它的优势在于在对于较⼩范围内的整数排序。
复杂度:
它的复杂度为Ο(n+k)(其中k是待排序数的范围),快于任何⽐较排序算法,缺点就是⾮常消耗空间。很明显,如果⽽且当O(k)>O(n*log(n))的时候其效率反⽽不如基于⽐较的排序,⽐如堆排序和归并排序和快速排序。
算法原理:
基本思想是对于给定的输⼊序列中的每⼀个元素x,确定该序列中值⼩于x的元素的个数。⼀旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。
例如,如果输⼊序列中只有17个元素的值⼩于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同⼀个位置上,在代码中作适当的修改即可。
算法步骤:
1.找出待排序的数组中最⼤的元素;
2. 统计数组中每个值为i的元素出现的次数,存⼊数组C的第i项;
3. 对所有的计数累加(从C中的第⼀个元素开始,每⼀项和前⼀项相加);
4. 反向填充⽬标数组:将每个元素i放在新数组的第C(i)项,每放⼀个元素就将C(i)减去1。
复杂度:
时间复杂度:Ο(n+k)。
空间复杂度:Ο(k)。
要求:待排序数中最⼤数值不能太⼤。
稳定性:稳定。
代码⽰例:
public class CountSort {
public static void main(String[] args) {
int arr[] = { 3, 1, 23, 3, 1, 4, 5, 199, 20 };
countSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void countSort(int[] arr){
int max = arr[0], min = arr[0];
for(int i = 1; i < arr.length; i++){
if(arr[i] > max){
max = arr[i];
}else if(arr[i] < min){
min = arr[i];
}
}
int[] count = new int[max - min + 1];
for(int i = 0; i < arr.length; i++){
count[arr[i] - min]++;
}
int k = 0;
for(int i = 0; i < count.length; i++){
for(int j = 0; j < count[i]; j++){
arr[k++] = i + min;
}
}
}
}
注意:计数排序是典型的以空间换时间的排序算法,对待排序的数据有严格的要求,⽐如待排序的数值中包含负数,最⼤值都有限制,请谨慎使⽤。
思想:
计数排序是一种非比较排序,它的基本思想是:统计待排序序列中每个元素出现的次数,然后根据元素的大小顺序依次输出。在代码中,我们首先通过一次遍历找到待排序序列中的最大值和最小值,然后创建一个计数数组count,用于存储每个元素出现的次数。具体地,我们遍历待排序序列,将每个元素的出现次数存储在对应的计数数组count中。然后,我们使用两个循环来输出排序结果。外层循环遍历count数组,内层循环遍历对应元素的出现次数,将元素按照出现次数依次输出。需要注意的是,在计数数组中,元素的下标i对应的是arr中的元素i + min。因此,在输出元素时,我们需要将下标i加上min才能得到正确的元素值。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
arr[k++] = i + min;
}
}
}
}
注意:计数排序是典型的以空间换时间的排序算法,对待排序的数据有严格的要求,⽐如待排序的数值中包含负数,最⼤值都有限制,请谨慎使⽤。
思想:
>
> 计数排序是一种非比较排序,它的基本思想是:统计待排序序列中每个元素出现的次数,然后根据元素的大小顺序依次输出。在代码中,我们首先通过一次遍历找到待排序序列中的最大值和最小值,然后创建一个计数数组count,用于存储每个元素出现的次数。具体地,我们遍历待排序序列,将每个元素的出现次数存储在对应的计数数组count中。然后,我们使用两个循环来输出排序结果。外层循环遍历count数组,内层循环遍历对应元素的出现次数,将元素按照出现次数依次输出。需要注意的是,在计数数组中,元素的下标i对应的是arr中的元素i + min。因此,在输出元素时,我们需要将下标i加上min才能得到正确的元素值。
>
>
[外链图片转存中...(img-AoaDIAGA-1715541117053)]
[外链图片转存中...(img-IDHZofX2-1715541117053)]
[外链图片转存中...(img-Fsa4xPWx-1715541117053)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**