对于算法,一直是一知半解,工作中极少会碰到,就算碰到,也是类似于其他的公司用了什么算法,我们调用什么接口,从来没有更深入的了解。
但是,对算法有一个比较清楚的框架还是很重要的,这一篇主要就是梳理算法的一些知识点。
1.算法简介
既然对算法一知半解,那么就应该从头开始了解算法,那么无非就是三个问题,从哪来,到哪去,干什么
很难对算法通过几句话甚至一篇文章对他做出解释,看到知乎上面的一篇文章,有了一定的思路,我们通常理解的算法一般被分为两种:
一种是传统的数据结构与算法,另一种是现在很热门的机器学习算法,传统数据结构算法偏法,机器学习算法偏算
对于数据结构和算法,套用计算机里面一句说烂了的话:
程序=数据结构+算法。
也就是说,通过算法将数据结构进行计算,就成了程序。
数据结构是数据组织的一种方式,算法的话可以举个很简单的例子:
计算1到100之间所有数的和,传统的最笨的方法就是逐个累加,即1+2=3,3+3=6,6+4=10,……,算到100就需要计算99次;这种方法极大的影响了程序的效率,尽管程序可以正常运行,但是并不是一个理想的选择。
但是我们可以找到规律:1+100=2+99=3+98=……=50+51,那不就是100/2=50个101相加嘛,所以直接等于(1+100)*100/2,即首末相加除以2就可以,是不是省了很多计算量,这在数量量更大时优势就更明显了,后面这种处理方式就是采用了某种算法。
算法就是将一些复杂的运算过程通过某种运算技巧进行更好的优化,为了实现这一步,算法利用了很多的数学技巧,所以,数学是算法的基础。
写程序就像开一辆车,当你不懂太多数据结构跟算法的时候,凭借丰富的实践经验你也可以将这辆车开好;
但是,当有一天这辆车出问题跑不起来的时候呢?
你不懂它内部的运行机制,你要怎么排除和解决问题?
对于机器学习算法,准确的说是应该称作模型而不能直接称为算法的。
机器学习的作用我们都熟悉,比如你经常用的淘宝购物猜你喜欢,网易云音乐推荐你感兴趣的歌曲,今日头条推送你感兴趣的新闻,人脸识别,语音识别,阿尔法狗与人下棋等等。
其实这些各种各样强大的模型背后基本都有一个共同点,那就是优化算法;说白了,机器学习模型大部分时候就是定义一个损失函数,将其转化为求解损失函数极小值的问题,你采用梯度下降法,牛顿法等各种优化方法,会直接决定模型的优化求解速度和准确度。
算法是程序的基础,在硬件相同的情况下,可以通过算法极大的优化程序性能,在初期我们可以通过编程语言使程序运行,但是在以后就需要算法让程序更好的运行。
2.简单算法基础
这一节是我们一般会接触到的简单通用的算法。
一般有八种常见算法:
看到一篇博文,决定不写了,改成转载了,很形象,一遍懂。
1.数据规模较小
(1)待排序列基本序的情况下,可以选择直接插入排序;
(2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡
2.数据规模不是很大
(1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出log(N)的额外空间。
(2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序
3.数据规模很大
(1)对稳定性有求,则可考虑归并排序
(2)对稳定性没要求,宜用堆排序
4.序列初始基本有序(正序),宜用直接插入,冒泡
排序类型 | 特点 |
---|---|
直接插入排序 | 在已有顺序上一个个直接插入,后面的依次与前面已排序的比较,合适则插入 |
希尔排序 | 先与后面的对比,一般是折半,然后再缩小对比半径,和插入的区别是插入一开始就是与旁边的对比。 |
简单选择排序 | 选择一个最小(大)的放在最前(后) |
堆排序 | 建堆,选择最大,剔出,剩下继续建堆,选择最大。。。。 |
冒泡排序 | 由下到上往后,碰到大的替换,继续向上 |
快速排序 | 选取基准,递归排序就是选个基准,比基准大的丢后面,小的扔前面然后,向下递归, |
归并排序 | 归并的思想在于:将数据缩小到最小单元,然后依次往上对比,排序 |
基数排序 | 先比较个位,再比较十位,再依次上升。 |
//以上是个人整理
//以下是转载原文+笔记:
2.1冒泡排序(Bubble Sort)
冒泡排序是一种极其简单的排序算法,也是我所学的第一个排序算法。它重复地走访过要排序的元素,一次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
- 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
由于它的简洁,冒泡排序通常被用来对于程序设计入门的学生介绍算法的概念。冒泡排序的代码如下:
public class bubbleSort {
public bubbleSort(){
int a[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,56,17,18,23,34,15,35,25,53,51};
int temp=0;
for(int i=0;i<a.length-1;i++){
for(int j=0;j<a.length-1-i;j++){
if(a[j]>a[j+1]){
temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}
}
for(int i=0;i<a.length;i++)
System.out.println(a[i]);
}
}
用两个动态图来展现:
//笔记:很清楚,大的换大的,就像那个小学文章,找到大的就丢了小的。一轮轮下来就排序好了。
冒泡排序的改进:鸡尾酒排序
鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。
2.2选择排序(Selection Sort)
选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
代码如下:
public class selectSort {
public selectSort(){
int a[]={1,54,6,3,78,34,12,45};
int position=0;
for(int i=0;i<a.length;i++){
int j=i+1;
position=i;
int temp=a[i];
for(;j<a.length;j++){
if(a[j]<temp){
temp=a[j];
position=j;
}
}
a[position]=a[i];
a[i]=temp;
}
for(int i=0;i<a.length;i++)
System.out.println(a[i]);
}
}
选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。
比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。
2.3插入排序(Insertion Sort)
插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌
对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
插入排序的代码如下:
package com.njue;
public class insertSort {
public insertSort(){
inta[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,56,17,18,23,34,15,35,25,53,51};
int temp=0;
for(int i=1;i<a.length;i++){
int j=i-1;
temp=a[i];
for(;j>=0&&temp<a[j];j--){
a[j+1]=a[j]; //将大于temp的值整体后移一个单位
}
a[j+1]=temp; //要j+1而不是j的原因是因为,for循环有一个j--操作
}
for(int i=0;i<a.length;i++)
System.out.println(a[i]);
}
}
插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
插入排序的改进:二分插入排序
对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目,我们称为二分插入排序,代码如下
当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。
2.4希尔排序(Shell Sort)
希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
先比较距离远的元素,而不是像简单交换排序算法那样先比较相邻的元素,这样可以快速减少大量的无序情况,从而减轻后续的工作。被比较的元素之间的距离逐步减少,直到减少为1,这时的排序变成了相邻元素的互换。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n^2)的排序(冒泡排序或直接插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
public class shellSort {
public shellSort(){
int a[]={1,54,6,3,78,34,12,45,56,100};
double d1=a.length;
int temp=0;
while(true){
d1= Math.ceil(d1/2); //对半操作
int d=(int) d1;
for(int x=0;x<d;x++){
for(int i=x+d;i<a.length;i+=d){
//因为for循环后i+=d,实际上j就是i
int j=i-d;
temp=a[i];
for(;j>=0&&temp<a[j];j-=d){
a[j+d]=a[j];
}
//因为 j-=d,所以此处实际就为j
a[j+d]=temp;
}
}
if(d==1)
break;
}
for(int i=0;i<a.length;i++)
System.out.println(a[i]);
}
}
代码可能不太容易理解,我们分析一下:
希尔排序存在随机性,而随机性的原因就是他选择和后面的相比
第一步:先折半分成两个部分,中间为参考点。然后第一个与参考点后的一个对比。第二个和参考点第二个对比。根据情况对换。大概这样,这样就跳过了多个数据。如果正好前大后小,则相当于省去了两个对比步骤。
第二步:缩小对比间隔( d1= Math.ceil(d1/2); )图没画完,大概意思就是这样
希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。
比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2时分成两个子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序之前第二个子序列中的8在前面,现在对两个子序列进行插入排序,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,两个8的相对次序发生了改变。
2.5 归并排序(Merge Sort)
归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),1945年由冯·诺伊曼首次提出。
归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。
归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
归并排序的代码如下:
import java.util.Arrays;
public class mergingSort {
int a[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,56,17,18,23,34,15,35,25,53,51};
public mergingSort(){
sort(a,0,a.length-1);
for(int i=0;i<a.length;i++)
System.out.println(a[i]);
}
//这是一个递归调用操作,一直递归到最小单元,然后依次向上合并
public void sort(int[] data, int left, int right) {
// TODO Auto-generated method stub
if(left<right){
//找出中间索引
int center=(left+right)/2;
//对左边数组进行递归
sort(data,left,center);
//对右边数组进行递归
sort(data,center+1,right);
//合并
merge(data,left,center,right);
}
}
//真正的排序操作实际上就是合并操作中的一个部分
public void merge(int[] data, int left, int center, int right) {
// TODO Auto-generated method stub
//创建一个数据用于存储排列数组
int [] tmpArr=new int[data.length];
int mid=center+1;
//third记录中间数组的索引
int third=left;
int tmp=left;
//注意以下有四个while
while(left<=center&&mid<=right){
//从两个数组中取出最小的放入中间数组
//一开始是两个最基本的,两两对比
//然后是四个,八个
//这个if是判断后,如果左小于右,则左放入新数组的左
if(data[left]<=data[mid]){
tmpArr[third++]=data[left++];
}else{
tmpArr[third++]=data[mid++];
}
}
//剩余部分依次放入中间数组
while(mid<=right){
tmpArr[third++]=data[mid++];
}
while(left<=center){
tmpArr[third++]=data[left++];
}
//将中间数组中的内容复制回原数组
//每次都会对原数组有此操作
while(tmp<=right){
data[tmp]=tmpArr[tmp++];
}
System.out.println(Arrays.toString(data));
}
}
2.6堆排序(Heapsort)
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构(通常堆是通过一维数组来实现的),并同时满足堆的性质:即子结点的键值总是小于(或者大于)它的父节点。
我们可以很容易的定义堆排序的过程:
- 创建一个堆
- 把堆顶元素(最大值)和堆尾元素互换
- 把堆的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
- 重复步骤2,直到堆的尺寸为1
堆排序的代码如下:
import java.util.Arrays;
public class HeapSort {
int a[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,56,17,18,23,34,15,35,25,53,51};
public HeapSort(){
heapSort(a);
}
public void heapSort(int[] a){
System.out.println("开始排序");
int arrayLength=a.length;
//循环建堆
for(int i=0;i<arrayLength-1;i++){
//建堆
//建堆即将最大的元素放在堆顶
//每次建的都是上一次排除最大后数据形成的堆
buildMaxHeap(a,arrayLength-1-i);
//交换堆顶和最后一个元素
swap(a,0,arrayLength-1-i);
System.out.println(Arrays.toString(a));
}
}
private void swap(int[] data, int i, int j) {
// TODO Auto-generated method stub
int tmp=data[i];
data[i]=data[j];
data[j]=tmp;
}
//对data数组从0到lastIndex建大顶堆
//堆我们可以通过集合创建,也可以通过数组创建
//此处是数组法,那么他们的左右节点又是什么呢?
//左右节点由数组的下标来判断,
//父节点:最后一个节点取半:(lastIndex-1)/2
private void buildMaxHeap(int[] data, int lastIndex) {
// TODO Auto-generated method stub
//从lastIndex处节点(最后一个节点)的父节点开始
for(int i=(lastIndex-1)/2;i>=0;i--){
//k保存正在判断的节点
int k=i;
//如果当前k节点的子节点存在
//注意,这里是while
while(k*2+1<=lastIndex){
//k节点的左子节点的索引
int biggerIndex=2*k+1;
//如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在
if(biggerIndex<lastIndex){
//若果右子节点的值较大
if(data[biggerIndex]<data[biggerIndex+1]){
//biggerIndex总是记录较大子节点的索引
biggerIndex++;
}
}
//如果k节点的值小于其较大的子节点的值
if(data[k]<data[biggerIndex]){
//交换他们
swap(data,k,biggerIndex);
//将biggerIndex赋予k,开始while循环的下一次循环
//重新保证k节点的值大于其左右子节点的值
k=biggerIndex;
}else{
break;
}
}
自己画的,有点简陋。。。。。。。。。。
2.7快速排序(Quicksort)
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:
从序列中挑出一个元素,作为”基准”(pivot).
把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
- 对每个分区递归地进行步骤1~3,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
public void quickSort(int[]a,int start,int end){
if(start<end){
int baseNum=a[start];//选基准值
int midNum;//记录中间值
int i=start;
int j=end;
do{
while((a[i]<baseNum)&&i<end){
i++;
}
while((a[j]>baseNum)&&j>start){
j--;
}
if(i<=j){
midNum=a[i];
a[i]=a[j];
a[j]=midNum;
i++;
j--;
}
}while(i<=j);
if(start<j){
quickSort(a,start,j);
}
if(end>i){
quickSort(a,i,end);
}
}
}
快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。
2.8基数排序(桶排序)
用于大量数,很长的数进行排序时。
将所有的数的个位数取出,按照个位数进行排序,构成一个序列。
将新构成的所有的数的十位数取出,按照十位数进行排序,构成一个序列。
public void baseSort(int[] a) {
//获取最大值,由最大值判断的次数
int max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
//首先确定排序的趟数;
int time = 0;
//判断位数;
while (max > 0) {
max /= 10;
time++;
}
//建立10个队列;
//分别对应1~10,用于存储一个位数上相同的元素,例如10,20,30或31,51,81
List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++) {
ArrayList<Integer> queue1 = new ArrayList<Integer>();
queue.add(queue1);
}
//进行time次分配和收集;
for (int i = 0; i < time; i++) {
//分配数组元素;
for (int j = 0; j < a.length; j++) {
//得到数字的第time+1位数;
int x = a[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
ArrayList<Integer> queue2 = queue.get(x);
queue2.add(a[j]);
queue.set(x, queue2);
}
int count = 0;//元素计数器;
//收集队列元素;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size() > 0) {
ArrayList<Integer> queue3 = queue.get(k);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
//收集个位,再收集十位:
//先比较个位,忽略十位,将个位从小到大排列,再比较十位,将十位从小到大排列
3.总结
这篇文章大多数是转载,在基于自己的一些理解完成,是对算法的一个小总结和梳理,当然,都是最简单,最基础的算法。
转载:
http://blog.csdn.net/pochenpiji159/article/details/62455478
参考:
https://www.zhihu.com/question/61869224
https://www.jianshu.com/p/5e171281a387
http://blog.csdn.net/without0815/article/details/7697916
看到一篇很详细的Java实现排序的文章:
java实现八种排序算法并测试速度(详细)