一直用框架会降智,算法才是根本
特性
有穷性:能算完
确定性:能算准
可行性:钱
基于数学建模通过代码实现解决问题
时间复杂度:快
算法运行时间只依赖于数据的输入规模
编程实现,设计实现
排序
选择排序
适用数组(适用,但不是很适用)链表
A[n]
i=0,j=0,t=0;
for(i<n;i++){
for(j=i+1;j<n;j++){
if(A[j]<A[i]){
t=A[i];
A[i]=A[j];
A[j]=t;
}
}
}
时间复杂度四舍五入是n^2,原式 n^2/2-n/2
冒泡排序
数组
A[n]
i=0,j=0,t=0;
for(i=n-1;i>0;i--){
for(j=1;j<i;j++){
if(A[j]<A[j+1]){
t=A[j];
A[j]=A[j+1];
A[j+1]=t;
}
}
}
也是双循环,所以复杂度也是n^2
目前来说,影响时间复杂度的就是循环所有数据的次数,且层数大于次数。
插入排序
时间复杂度N^2
从后往前比较,每个元素和前一位比较,后边的小就放前面一位。也是相
邻的两两相比,方向和冒泡是相反的。
package com.gg.demo2;
import java.util.Arrays;
public class Insertion { // 交换函数
public static void swap(int[] array, int a, int b) {
array[a] = array[a] + array[b];
array[b] = array[a] - array[b];
array[a] = array[a] - array[b];
}
public static void main(String[] args) {
int [] a = {8,7,6,5,4,3,2,1};
//数组有多少个位置, 就需要循环多少次每个位置最终决定放谁
for (int i = 0; i < a.length; i++) {
//j 表示当前元素索引 , j-1 当前元素前一个位置的索引
// 所以 j-1 临界值是0 所以j>0
// j--: 因为当前位置的元素 每次都是和它前一个进行比较
for (int j = i; j >0; j--) {
//如果当前位置元素 比前一个位置的元素小就交换
if(a[j]<a[j-1]){
swap(a, j, j-1);
}
}
}
System.out.println(Arrays.toString(a));
}
}
数组链表
n^2
public static void swap(int[] array, int a, int b) {
array[a] = array[a] + array[b];
array[b] = array[a] - array[b];
array[a] = array[a] - array[b];
}
public static void main(String[] args) {
int [] a = {8,7,6,5,4,3,2,1};
//数组有多少个位置, 就需要循环多少次每个位置最终决定放谁
for (int i = 0; i < a.length; i++) {
//j 表示当前元素索引 , j-1 当前元素前一个位置的索引
// 所以 j-1 临界值是0 所以j>0
// j--: 因为当前位置的元素 每次都是和它前一个进行比较
for (int j = i; j >0; j--) {
//如果当前位置元素 比前一个位置的元素小就交换
if(a[j]<a[j-1]){
swap(a, j, j-1);
}
}
}
System.out.println(Arrays.toString(a));
}
每个数比较前一个,小的放前面
默认是双循环了\
希尔排序(Shell Sort)
时间复杂度: N^1.3
自定义一个增量gap , 但是这个增量是递减的, 一般取gap = len /2 除任意常数,len越大可以让常数增大一些。
希尔排序可以有效的减少 元素位置之间的 交换次数, 通过小分组组内的插入排序使其交换次数变少。
-
自定义增量。
int n;
-
按增量的数值进行分组。
a=b+n;a放左边,b放右边
-
每个组内要进行插入排序。
-
当gap =1 时,只分1组进行插入排序了。
减小上面的n,不断减小到1(单位数)
-
当gap=0时 , 程序结束了,得到结果。
因为在gap=1时所有相邻的元素都是大小相邻的了
java:
package com.gg.demo2; import java.util.Arrays; public class Shell { // 交换函数 public static void swap(int[] array, int a, int b) { array[a] = array[a] + array[b]; array[b] = array[a] - array[b]; array[a] = array[a] - array[b]; } public static void main(String[] args) { int [] a = {9,8,7,6,5,4,3,2,1}; // gap : 初始值是 长度/2 ,每次递减一半, 循环条件 gap !=0 for (int gap= a.length/2; gap>0; gap/=2) { //确定从哪开始的位置元素是需要进行插入排序的, //因为每个小分组的第一个元素前面没元素了,没法比, 所以都需要把它抛出去 //发现规律, 正好需要进行比的那个元素们起始索引值就是gap // a.length: 从gap值索引开始往后的一致到a.length-1都需要在其自己组内进行比 较。 for (int i =gap; i < a.length; i++) { //进行插入排序 //当前元素索引是j, 要比的前一个元素索引是j-gap //j=j-gap :下次进行比较的 当前元素 和 前一个元素 也是差了 gap步 // j-gap是 索引 要>=0 // a[j]<a[j-gap] 如果当前分组中, 当前位置比它前一个位置的小,就交换。 // 前一个位置和当前位置 相差gap 步 for (int j = i;j-gap>=0&&a[j]<a[j-gap] ; j=j-gap) { swap(a, j, j-gap); } } }System.out.println(Arrays.toString(a)); } }
分治策略
比如: 二分查找法、快速排序、归并排序 都是 基于 分治策略的。
三步:
1.拆分大问题 。
2.把所有的小问题进行求解。
3.把所有的解进行合并。
分治策略一定会使用递归的。
递归就是 函数自身调用了自身, 有递归就一定要有出口,否则程序无法返回。
分: 分就是拆分的过程。
治: 求解过程和合并解的过程。
归并排序(Merge Sort)
时间复杂度:N*LogN
把序列进行拆分,不是真正拆解原数组, 而是根据索引范围进行拆解。
拆到元素长度为1时就停止,开始进行排序了。 拆的过程其实就是在定义合的过程。
也是使用了二分法进行拆分的。
图片在我的电脑上丢了,有空再补吧。
在治的过程中, 我们发现, 两个带子序的数组进行合并,有一个数组子序算法。
并不是两个数组在排序合并,而是用索引进行表示的所谓范围。
定义三个索引, i 指向 开头 start, j指向结尾end , k指向 了临时数组的开头。
还需要定义一个 临时数组。步骤如下:
数组子序算法:
-
创建一个和 原数组等大的 临时数组。
-
定义i,j,k . 同上。
-
三种情况,两个范围的数组分别称为A,B
-
A没走完,B没走完, 才会比较, 把小的方到temp数组中。
-
A没走完, B走完了, 把A剩下的都放到temp
-
A走完了, B没走完, 把B剩下的都放到temp
4.把temp中排序完了的结果copy给原数组, 覆盖掉原数组中的值。
package com.gg.demo2;
import java.util.Arrays;
public class Merge {
// 交换函数
public static void swap(int[] array, int a, int b) {
array[a] = array[a] + array[b];
array[b] = array[a] - array[b];
array[a] = array[a] - array[b];
}
public static void main(String[] args) {
int[] a = {8,7,6,5,4,3,2,1};
int start = 0 ;
int end = a.length-1;
split(a,start, end);
System.out.println(Arrays.toString(a));
}
//分的过程
private static void split(int[] a, int start, int end) {
//出口
if(start>=end){
return ;
}
// TODO 自动生成的方法存根
int mid = (start+end)/2;
//拆出来的左面
split(a, start, mid);
//拆出来的右面
split(a, mid+1, end);
//合并解
//start-------mid , mid+1----end 分别表示每次合并的是哪两个。
//start, mid , end都是变化的。
merge(a,start,mid,end);
}
private static void merge(int[] a, int start, int mid, int end) {
// TODO 自动生成的方法存根
//1创建 临时数组
int [] temp= Arrays.copyOf(a, a.length);
//2 定义i,j,k 笔记中有
int i = start ;
//A数组头
int j = mid+1 ;
//B数组头
int k = 0 ;
//临时数组头
//3种情况
while(i<=mid && j<=end){
if(a[i]<a[j]){
temp[k++] = a[i++];
}
else{temp[k++]=a[j++];
}
}
while(i>mid && j<=end){
temp[k++]=a[j++];
}
while(i<=mid && j>end){
temp[k++]=a[i++];
}
//4 覆盖原数组
// j=0:J就是个计数器了,k是往临时数组中放了多少个元素,没清零。j要往元数组中覆盖k次元素。
//i=start: 元素组每次都不一样, 但是都是从start开始的。 a[i]表示的是原数组的开始元素
for (i=start,j=0;j<k; j++,i++) {
a[i] = temp[j];
}
}
}
第二种方法:
package com.gg.demo;
import java.util.Arrays;
public class GuiBing2 {
public static int[] merge(int[] left, int[] right){
int [] temp = new int[left.length+right.length];
int i = 0 ;
//1
while(left.length>0 && right.length>0){
if(left[0]<right[0]){
temp[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
else{
temp[i++] = right[0];
right= Arrays.copyOfRange(right, 1, right.length);
}
}
//2
while(right.length>0){
temp[i++] = right[0];
right= Arrays.copyOfRange(right, 1, right.length);
}
while(left.length>0){
temp[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
return temp ;
}
public static int [] split(int [] a){
int [] temp = Arrays.copyOf(a, a.length);
int mid = temp.length/2 ;
if(a.length<2){
return temp ;
}
int [] left = Arrays.copyOfRange(temp, 0, mid);
int [] right = Arrays.copyOfRange(temp, mid, a.length);
//
return merge(split(left), split(right));
}
public static void main(String[] args) {
int [] a = {8,7,6,5,4,3,2,1};
int [] res = split(a);
System.out.println(Arrays.toString(res));
}
}
桶排序
非基于比较,时间复杂度非常低。空间换时间。
1计数排序
时间复杂度:O(n+k),n是数据规模,k是最大数据元素
排序规则,
1,遍历原数组
2,求出原数组中 的 最大值。 最大值K决定了 桶数组的长度。
int [] ]helper = new int [source.lengt+1];
3, 将原数组元素的值 放入到 桶元素对应下标 ++操作。
4, 重新遍历桶数组, 完成排序。
不是基于 比较 进行排序
桶, Bucket
桶 是用来 计数的
统计 每个数值在原数组中 出现过的次数。
计算排序的时间复杂度
N 数据规模 O(N+k)
K 就是 原数组中的最大值
用空间换取时间
适用于 最大值 和数据规模,差距并不是很大的时候
package com.gg.demo2;
import java.util.Arrays;
public class Counting {
public static int getMin(int [] a){
int min = getMax(a);
for (int i : a) {
if(min>i){
min = i ;
}
}
return min ;
}
public static int getMax(int [] a){
int max = Integer.MIN_VALUE;
for (int j : a) {
if(max <j){
max = j ;
}
}
return max;
}
public static void main(String[] args) {
int [] a = {8,8,8,4,6,5};
//1 获取原数组中的最大值
int max = getMax(a);
//1.2 获取原数组中的最小值
int min = getMin(a);
//2 设计我桶们
int [] bucket = new int [max+1-min];
//3 扫描原数组 桶开始 计数
for (int i = 0; i < a.length; i++) {
bucket[a[i]-min]++;
}
//4 扫描桶们, 如果当前的编号的桶值不是0的话,说明,有值。 一直到当前编号的桶=0;
int k = 0 ;
//指向了原数组
for (int i = 0; i < bucket.length; i++) {
while(bucket[i]>0){
a[k++] = i+min ;
bucket[i] -- ;
}
}
System.out.println(Arrays.toString(a));
}
}
基数排序
时间复杂度:O(n*k)
k是最大数的位数。
基数: 位 个 、十、百、千、万…
桶: 当前的原数组中进制是多少 来决定, 比如 十进制 => 10桶, 0-9
1.先确定原数组中的最大值
2. 确定这个最大值的位数, 如果是4位, 那么 进行4次循环。
3. 取第d位 依次放入 对应的桶中,
4.遍历所有的桶,覆盖掉原数组, 直到d =最大数的位。
package com.gg.demo2;
import java.util.Arrays;
public class Radix {
public static int getMax(int [] a){
int max = Integer.MIN_VALUE;
for (int i : a) {
if(max<i){
max = i ;
}
}
return max ;
}
public static int getMaxLen(int max){
return String.valueOf(max).length();
}
//按位 取某个数。
public static int getDigit(int number , int d){
int [] array = {1,10,100,1000,10000,100000,1000000,10000000,100000000};
// number / (int)Math.pow(10, d) %10 ;
return number / array[d] %10 ;
}
public static void main(String[] args) {
int [] a = {9999,876,456,222,22,11,67,13,9,2,1,7,4,4,4,0,0,0};
//1 先要取到 数组中的最大值
int max = getMax(a);
//2 取到最大位的位数(长度)
int maxLen = getMaxLen(max); //3 循环MaxLen次
for (int i = 0; i < maxLen; i++) {
//制作桶 //10个元素,每个元素都是一个一位数组 , 每个一位数组长度都是3
//最极端情况, 按位排序,原数组中所有的元素 当前位都一样。
Integer [][] bucket = new Integer [10][a.length];
//3.1 遍历原数组, 按照余数放到对应的桶里。
for (int j = 0; j < a.length; j++) {
//将原数组当前元素 放入到对应的桶里
int yu =getDigit(a[j], i);
bucket[yu][j] = a[j] ;
}
int k = 0 ;
//指向了原数组
//扫描桶, 覆盖原数组
for (Integer [] temp : bucket) {
for (int j = 0; j < temp.length; j++) {
if(temp[j]!=null){
a[k++] = temp[j];
}
}
}
}
System.out.println(Arrays.toString(a));
}
}
桶排序(哈希)
可以对小数也能排序。
计算每个桶之间的跨度
(最大值-最小值)/(桶数量-1)
桶: 表示的是 每个数值的范围
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56Ytfrkx-1678366564895)(C:\Users\zhangyunli\AppData\Roaming\Typora\typora-user-images\image-20211025140616031.png)]
1. 定义使用多少个桶。 划分 每个桶的区间。
2. 每个桶桶内要进行 排序 。Collections.sort() 底层使用了归并
3. 把所有的桶的数据按顺序输出即可 得到最后的排序结果。
package com.gg.demo2;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class Bucket {
public static void main(String[] args) {
double[] a = { 3.14, 3.33, 1.11, 56.5, 77.8, 122.1, 8, 7, 6, 5, 4, 3, 3, 2, 1 };
sort(a);
System.out.println(Arrays.toString(a));
}
public static double getMin(double[] a){
double min = getMax(a);
for (double i : a) {
if(min>i){
min = i ;
}
}
return min ;
}
public static double getMax(double[] a){
double max = Integer.MIN_VALUE;
for (double i : a) {
if(max<i){ max =i ;
}
}
return max ;
}
//(max - min )/(bucketCount -1) 求出 桶之间的区间跨度
private static void sort(double[] a) {
double max = getMax(a);
double min = getMin(a);
// TODO 自动生成的方法存根
// 我希望 使用 5个桶 。
int bucketCount = 5 ;
//生产 5个 桶 ,把5个桶放到一个大集合里
List<LinkedList<Double>> buckets = new ArrayList<LinkedList<Double>>();
for (int i = 0;
i <5; i++) {
buckets.add(new LinkedList<Double>());
}
// 开始 遍历 原数组 , 根据原数组当前元素的值, 来区分它需要落到哪个桶里。
for (double number : a) {
// 求出这个number 需要落到 哪个桶编号里。
//min 值肯定落到0号桶 里
// number -min 就是 一共大了多少数值 ,
// 多出来的数值 除 每个桶数值跨度 === 一共能跨多少个桶。
int idx = (int) ((number-min)*(bucketCount -1)/(max - min ));
//从桶集合中按编号找到这个桶, 把元素放到这个桶里
buckets.get(idx).add(number);
}
// 把5个桶 ,桶内自行排序
for (LinkedList<Double> bucket : buckets) {
//底层归并排序
Collections.sort(bucket);
}
//遍历所有的桶, 从0号桶开始最后一个桶 , 覆盖掉原数组a
//指向了 原数组 0索引 (开头位置)
int i = 0 ;
for (LinkedList<Double> bucket : buckets) {
for (Double d : bucket) {
a[i++] = d;
}
}
}
}
快速排序(Quick Sort)
基于 分支策略
是一个不稳定的排序方法。
时间复杂度是 LogN*N ~ N^2
Java的API 都是使用了快速排序。
快速排序 对于 偏向于随机序列 有 优势。
定义一个主元 pivot
所有的数 都和 主元 进行比较
定义了 2个哨兵 指针
i => 当前阶段 比主元小的序列的最后一个位置。
j=> 指向了每次的元素
(1)如果a[j] >=a[pivot] j++的操作。
(2)如果a[j]<a[pivot]
swap(a, i+1,j);
i++;
j++;
一旦 j 走到头, 操作: swap(a, i+1,pivot)
使用三数取中法 解决在选取主元时 避开最大或者最小。
package com.gg.demo;
import java.util.Arrays;
public class KuaiPai {
public static void getPivot(int [] a , int start , int end){
System.out.println(Arrays.toString(a));
int number1 = a[start];
int number2 = a[(start+end)/2] ;
int number3 = a[end];
int [] temp = {number1, number2, number3};
for (int i = 0; i < temp.length; i++) {
for (int j = i+1; j < temp.length; j++) {
if(a[i]>a[j]){ swap(temp, i, j);
}
}
}
a[start] = temp[0];
a[(start+end)/2] = temp[1];
a[end] = temp[2];
//
swap(a, (start+end)/2, end-1);
System.out.println(Arrays.toString(a));
}
public static void swap(int []array, int a, int b){
int temp = array[a];
array[a] = array[b];
array[b] = temp ;
}
public static void sort(int[] a, int start , int end ){
//当前分支长度==1 递归出口 结束
if(start>=end){ return ;
}
getPivot(a, start, end);
//1 确定 pivot
int pivot = end -1 ;
//
int i = start - 1;
// 遍历当前start => end所有的元素,和主元比较
for (int j = start; j <end-1; j++) {
//如果 当前位置元素比 主元 小,
if(a[j]<a[pivot]){
//就把 当前元素和 i+1位置的元素 交换
swap(a, j, i+1);
// i指针 指向最新的 比主元小的那个数,确保 循环结束时刻和 主元交换。
i++ ;
}
}
//把主元和i+1 位置的元素 互换, 保证左侧都比主元小, 右侧都比主元大。
swap(a, pivot, i+1);
//左分支
sort(a, start, i+1);
//右有分支
sort(a, i+2, end);
}
public static void main(String[] args) {
int [] a = {8,7,6,5,4,3,2,1};
int start = 0 ;
int end = a.length-1;
sort(a,start,end);
System.out.println(Arrays.toString(a));
}
}
堆排序
时间复杂度: N*LogN
堆: 堆积,含义是一定是按照数值的大小进行排列。
大堆:双亲要比孩子数值大。
小堆:双亲要比孩子数值小。
堆被建立出来之后,一定是一个完全二叉树的结构。
往往都是基于大堆的, 最后得到的排序结果是从小到大的。
反之小堆排列的,是从大到小的。
**大顶堆:**arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
**小顶堆:**arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]