1、排序
排序分类:
1)内部排序:
值将需要处理的所有数据都加载到内部存储器中进行排序。
2)外部排序:
数据量大,无法全部加载到内存中,需要借助外部存储进行排序。
1.1冒泡排序(Bubble Sorting)
1.2选择排序(Select Sorting)
基本思想:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
假如给定初始数据:(118,101,105,127,112)
一次排序:101,118,105,127,112
二次排序:101,105,118,127,112
三次排序:101,105,112,127,118
四次排序:101,105,112,118,127
1)一共有数组大小-1轮排序
2)每一轮排序,又是一个循环
2.1)先假定当前这个数是最小数
2.2)然后与后面的每一个数进行比较,若发现有比当前数更小的数,就重新确定最小数,并得到下标
2.3)当遍历到数组的最后时,就得到本轮的最小数和下标
public static int[] Select(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if(min > arr[j]) {
min = arr[j];
minIndex = j;
}
}
if(minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
}
return arr;
}
1.3插入排序(Insert Sorting)
插入排序是对预排序的元素以插入的方式找该元素的适当位置以达到排序的目的。
基本思想:把n个待排序的元素看成一个有序表和一个无序表,开始时有序表只包含一个元素,无序表中包含n-1个元素,排序过程中每次从无序表中取出第一个元素与有序表中的元素依次进行比较,将它插入到有序表中的适当位置使之成为新的有序表。
假如给定初始数据:(【118】,101,105,127,112)
【】为有序表
一次排序:【101,118】,105,127,112
二次排序:【101,105,118】,127,112
三次排序:【101,105,112,127】,118
四次排序:【101,105,112,118,127】
public static int[] insert(int[] arr) {
int indexVal = 0;
int index = 0;
for (int i = 1; i < arr.length; i++) {
indexVal = arr[i];
index = i - 1;
while(index >= 0 && indexVal < arr[index]) {
arr[index + 1] = arr[index];
index--;
}
arr[index + 1] = indexVal;
}
return arr;
}
1.4希尔排序(Shell’s Sort)
在插入排序中,若数组最后一位数最小(arr = {8,9,1,7,2,3,5,4,6,0}),此时,最后一位最小的数后移次数过多,对代码执行的效率将会产生影响。
这时便可以采用希尔算法。
希尔算法(缩小增量排序)基本思想:把记录按下标的一定增量分组,对每一组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法结束。
最后只需要对以上数列简单微调无需大量移动操作即可完成整个数组的排序。
实现希尔排序有两种方法:
1)交换式(效率低,甚至比原本的插入排序还要慢)
public static int[] exshell(int[] arr) {
int temp = 0;
for (int k = arr.length / 2; k > 0; k /= 2) {
for (int i = k; i < arr.length; i++) {
for (int j = i - k; j >= 0; j -= k) {
if (arr[j] > arr[j + k]) {
temp = arr[j];
arr[j] = arr[j + k];
arr[j + k] = temp;
}
}
}
}
return arr;
}
2)移位式
public static int[] moveshell(int[] arr) {
int indexVal = 0;
int index = 0;
for (int k = arr.length / 2; k > 0; k /= 2) {
for (int i = k; i < arr.length; i++) {
index = i;
indexVal = arr[index];
if(arr[index] < arr[index - k]) {
while((index - k) >= 0 && indexVal < arr[index - k]) {
arr[index] = arr[index - k];
index -= k;
}
arr[index] = indexVal;
}
}
}
return arr;
}
1.5快速排序(Quick Sort)
快速排序是对冒泡排序的一种改进。基本思想:通过一趟排序将要排序的数据分割成独立的部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这俩部分数据分别进行快速排序,整个排序过程可以递归进行。
public static void quite(int[] arr,int left,int right) {
int l = left;
int r = right;
int temp = 0;
int mid = arr[(left + right) / 2];
while(l < r) {//确保mid左边的值都小于它右边的值都大于它
while(arr[l] < mid) {//在左边找到比mid大的值
l++;
}
while(arr[r] > mid) {//在右边找到比pivot下的值
r--;
}
if(l == r) {//若满足,则pivot左边的值都小于它右边的值都大于它
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
//第一次分割后[-6, -9, -10, 0, 6, 2, 8, 1, 7]
//此时l==r,接下来需要左递归和右递归进行下一次分割
//这样就需要将左递归的右边界变为-10的下标也就是2
//右递归的左边界变为-6的下标也就是4
if(l == r) {
l++;
r--;
}
//向左递归
if(left < r) {
quite(arr, left, r);
}
//向右递归
if(right > l) {
quite(arr, l, right);
}
}
1.6归并排序(Merge Sort)
归并排序是利用归并的思想实现的排序方法,采用分治策略。
分治法:将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案“修补”在一起。
并:
public class MergeSort {
public static void main(String[] args) {
int[] arr = {8,4,5,7,1,3,6,2};
int[] temp = new int[arr.length];
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println(Arrays.toString(arr));
}
//分+并
public static void mergeSort(int[] arr,int left,int right,int[] temp) {
int mid = (left + right) / 2;
if(left < right) {
//左递归
mergeSort(arr, left, mid, temp);
//右递归
mergeSort(arr, mid + 1, right, temp);
//每递归一次就合并
merge(arr, left, mid, right, temp);
}
}
/**
*
* @param arr 原始数组
* @param left 左边有序序列的索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 中转索引
*/
//并
public static void merge(int[] arr,int left,int mid,int right,int[] temp) {
int i = left;
int j = mid + 1;
int t = 0;
//将左右两边有序的数据按照规则填充到temp数组直到左右两边的有序序列有一边处理完成
while( i <= mid && j <= right) {
if(arr[i] <= arr[j]) {
temp[t] = arr[i];
t++;
i++;
}else {
temp[t] = arr[j];
t++;
j++;
}
}
//将剩余数据的一边依次全部填充到temp
while(i <= mid) {
temp[t] = arr[i];
t++;
i++;
}
while(j <= right) {
temp[t] = arr[j];
t++;
j++;
}
//将temp数组的元素拷贝到arr
t = 0;
int tempLeft = left;
while(tempLeft <= right) {
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
}
1.7基数排序(Radix Sort)
1)基数排序属于“分配式排序”,又称“桶子法”或bin sort,是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
2)基数排序是属于稳定性的排序,基数排序法是效率高的稳定性的排序
3)基数排序是桶排序的扩展
4)基数排序是将整数按位数切割成不同的数字,然后按每个位数分别比较。
基数排序使用空间换时间的算法,若需要排序的数太多会造成内存不足
基本思想:
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
图解:
初始状态:{53, 3, 542, 748, 14, 214}
说明:事先准备10个数组(10个桶),0-9 分别对应位数的 0-9
第1轮排序:
(1) 将每个元素的个位数取出,然后看这个数应该放在哪个对应的桶(一维数组)
(2) 按照这个桶的顺序(一维数组的下标)依次取出数据放入原数组
第1轮排序后:542 53 3 14 214 748
第2轮排序:
(1) 将每个元素的十位数取出,然后看这个数应该放在哪个对应的桶(一维数组)
(2) 按照这个桶的顺序(一维数组的下标)依次取出数据放入原数组
第2轮排序后: 3 14 214 542 748 53
第3轮排序:
(1) 将每个元素的百位数取出,然后看这个数应该放在哪个对应的桶(一维数组)
(2) 按照这个桶的顺序(一维数组的下标)依次取出数据放入原数组
第3轮排序后:3 14 53 214 542 748
public class RadixSort {
public static void main(String[] args) {
int[] arr = {53, 3, 542, 748, 14, 214};
radix(arr);
}
public static int[] radix(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {//取得数组中最大的数
if (arr[i] > max) {
max = arr[i];
}
}
int maxLength = (max + "").length();//获取最大位数
//定义一个二维数组表示桶
int[][] bucket = new int[10][arr.length];
//定义一个一位数组表示桶内数组
int[] bucketInner = new int[10];
for (int i = 0,n = 1; i < maxLength; i++,n *= 10) {
for (int j = 0; j < arr.length; j++) {
int post = arr[j] / n % 10;//获取数的个、十、百...位数
bucket[post][bucketInner[post]] = arr[j];//将取出的数放入对应的桶中
bucketInner[post]++;
}
int index = 0;
//遍历每一个桶
for (int k = 0; k < bucketInner.length; k++) {
if (bucketInner[k] != 0) {
//将每个桶内的数依次取出来
for (int l = 0; l < bucketInner[k]; l++) {
arr[index] = bucket[k][l];
index++;
}
}
bucketInner[k] = 0;//每一轮处理完毕后都需要将每个bucketInner数组清零
}
System.out.println("第"+(i+1)+"轮,对应的数组为:" + Arrays.toString(arr));
}
return arr;
}
}
1.8堆排序()
1)堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序;
2)堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(arr[i] >= arr[2i+1 ] && arr[i] >= arr[2i+2]);
3)每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(arr[i] <= arr[2i+1 ] && arr[i] <= arr[2i+2])。
堆排序的基本思想:
1)将侍排序序列构造成一个大顶堆
2)此时,整个序列的最大值就是堆顶的根节点。
3)将其与末尾元素进行交换,此时末尾就为最大值。
4)然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
public class HeapSort {
public static void main(String[] args) {
int[] arr = {4,2,54,6,8,5,9};
heapSort(arr);
}
//堆排序方法
public static void heapSort(int[] arr) {
int temp = 0;
//形成大顶堆
for(int i = arr.length / 2 - 1;i >= 0;i--) {
adjustHeap(arr, i, arr.length);
}
//形成小顶堆
for(int i = arr.length / 2 - 1;i >= 0;i--) {
adjust(arr, i, arr.length);
}
//将堆顶元素与末尾元素交换(最大的数放在数组末端)
for(int j = arr.length - 1;j > 0;j--) {
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
System.out.println("大顶堆:" + Arrays.toString(arr));
//将堆顶元素与末尾元素交换(最大的数放在数组末端)
for(int j = arr.length - 1;j > 0;j--) {
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjust(arr, 0, j);
}
System.out.println("小顶堆:" + Arrays.toString(arr));
}
//将数组变为大顶堆形式
public static void adjustHeap(int[] arr,int i,int length) {
int temp = arr[i];
for(int k = i * 2 + 1;k < length;k = k * 2 + 1) {
//若左子节点小于右子节则将k指向右子节点
if(k + 1 < length && arr[k] < arr[k + 1]) {
k++;
}
//若已调整后的子节点大于它的父节点则将父节点的值换为子节点的值
if(arr[k] > arr[i]) {
arr[i] = arr[k];
i = k;//将k的值赋给i以供后面的交换使用
}else {
break;
}
}
arr[i] = temp;//将被父节点替换的子节点的值换成父节点原来的值
}
//将数组变为小顶堆形式
public static void adjust(int[] arr,int i,int length) {
int temp = arr[i];
for(int k = i * 2 + 1;k < length;k = k * 2 + 1) {
//若左子节点小于右子节则将k指向右子节点
if(k + 1 < length && arr[k] > arr[k + 1]) {
k++;
}
//若已调整后的子节点大于它的父节点则将父节点的值换为子节点的值
if(arr[k] < arr[i]) {
arr[i] = arr[k];
i = k;//将k的值赋给i以供后面的交换使用
}else {
break;
}
}
arr[i] = temp;//将被父节点替换的子节点的值换成父节点原来的值
}
}
总结:
稳定:若a原本在b前面,而a=b,排序后a仍然在b前面;
不稳定:若a原本在b前面,而a=b,排序后a可能会出现在b后面;
内排序:所有排序操作都在内存中完成;
外排序:由于数据大,因此将数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
时间复杂度:一个算法执行所耗费的时间;
空间复杂度:运行完一个程序所需内存的大小;
n:数据规模;
k:“桶”的个数
In-place:不占用额外内存;
Out-place:占用额外内存。
2、算法时间复杂度
2.1方法:
1)事后统计法
这种方式要在同一台计算机的相同状态下运行,才能比较哪个算法速度更快并且所得时间的统计量依赖于计算机的硬件、软件等环境因素。
2)事前估算法
通过分析某个算法的时间复杂度来判断哪个算法更优。
2.2时间频度
一个算法花费的时间与算法中语句的执行次数成正比,哪个算法中语句执行次数多花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
for循环计算了100(n)次最后判断是否小于end所以时间复杂度为n+1。
时间频度可以忽略参数项:
2n+20和2n随着n变大,数值无限接近,20可以忽略;
3n+10和3n随着n变大,数值无限接近,10可以忽略。
时间频度可以忽略低次项:
2n ^ 2 + 3n +10 和 2n ^ 2随着n变大,数值无限接近,可以忽略3n + 10;
n ^ 2 + 5n +20 和 n ^ 2随着n变大,数值无限接近,可以忽略5n + 20。
时间频度可以忽略系数:
随着n值变大,5n ^ 2 + 7n和3n ^ 2 + 2n,数值逐渐相等,这种情况下5和3可以忽略;
而n ^ 3 + 5n 和 6n ^ 3 + 4n,数值相差越来越大,说明多少次方是关键,这种情况下5和4可以忽略。
2.3时间复杂度
1)一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
2)T(n)不同,但时间复杂度可能相同,如:T(n) = n^2 + 7n + 6与T(n) = 3n ^ 2 + 2n + 2它们的T(n)不同,但时间复杂度相同,都为O(n ^ 2)。
3)计算时间复杂度的方法:
,用常数1代替运行时间中的所有加法常数:T(n) = n^2 + 7n + 6=>T(n) = n ^ + 7n + 1
修改后的运行次数函数中,只保留最高阶项:T(n) = n^2 + 7n + 1=>T(n) = n ^ 2
去除最高阶项的系数:T(n) = n ^ 2=>T(n) = n ^ 2=>O(n^2);
2.4常见时间复杂度
1)常数阶:
无论代码执行了多少行,只要没有循环等复杂结构,这个代码的时间复杂度就是O(1)
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
此代码执行时消耗并不随着某个变量的增长而增长,无论代码有多长,时间复杂度都为O(1)。
2)对数阶:
int i = 1;
while(i < n){
i = i * 2;
}
3)线性阶:
for(i = i ; i <= n ; ++i){
j = i;
j++;
}
for循环里的代码会执行n遍,因此消耗的时间随着n的变化而变化,因此这类代码时间复杂度都为O(n)。
4)线性对数阶:
for(m = 1;m < n;m++){
i = 1;
while(i < n){
i = i + 2;
}
}
5)平方阶:
for(x=1;i<=n;x++){
for(i=1;i<=n;i++){
j=i;
j++;
}
}
嵌套俩次for循环时间复杂度为O(n*n)=>O(n^2)
6)立方阶、k次方阶:3、k层n循环
2.5平均时间复杂度与最坏时间复杂度
1)平均时间复杂度是指所有可能的输入实例以等概率出现的情况下该算法的运行时间。
2)最坏情况下的时间复杂度称为最坏时间复杂度,一般讨论的时间复杂度均是最坏情况下的时间复杂度。这样做是因为:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
3)平均时间复杂度和最坏时间复杂度是否一致,和算法有关:
3、算法空间复杂度
1)类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数;
2)空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,有的算法需要占用的临时工作单元数与解决问题的规模n有关,它
随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归
并排序算法就属于这种情况;
3)在做算法分析时,主要讨论的是时间复杂度,从用户使用体验上看,更看重的
程序执行的速度,一些缓存产品(redis, memcache)和算法(基数排序)本质就是用
空间换时间。