基本排序算法

冒泡排序

基本思想:两两相邻进行比较,将最大的放到后面,每轮比较都会使需要比较的元素数目减一,总共需要比较length-1轮。由于嵌套for循环,时间复杂度最优和平均都是o(n^2),空间复杂度o(1)。冒泡排序是稳定的。

public class Main {

    public static void bubblingSort(int[] list) {
        int i, j;
        int length = list.length;
        for (i = length - 1; i > 0; i--) {
            for (j = 0; j < i; j++) {
                if (list[j] > list[j + 1])
                    swap(list, j, j + 1);
            }
        }
    }

    /**
     * 打印
     * @param list
     */
    private static void show(int[] list) {
        for (int i : list) {
            System.out.print(i + " ");
        }
        System.out.println();
    }


    /**
     * 交换
     * @param list
     * @param a
     * @param b
     */
    public static void swap(int[] list, int a, int b) {
        int temp = list[a];
        list[a] = list[b];
        list[b] = temp;
    }

    public static void main(String[] args) {
        int[] list = new int[] { 2, 17, 5, 1, 2, 8, 4, 13, 11, 6 };
        bubblingSort(list);
        show(list);
    }

}

优化点:如果数组为[3,2,4,5],其实在执行第一轮后,[2,3,4,5],数组已经有序,为了避免数组有序情况下继续执行循环,可以设定一个信号flag,改进代码如下

public static void bubblingSort(int[] list) {
    int i, j;
    int length = list.length;
    boolean flag=true;//设定信号量,初始化为true
    for (i = length - 1; i > 0&&flag; i--) {
        flag=false;//每一轮都将信号量置为flase
        for (j = 0; j < i; j++) {
            if (list[j] > list[j + 1]){
                swap(list, j, j + 1);
                flag=true;//该轮发生交换,继续执行下一轮;否则数组已经有序,跳出循环
            }
        }
    }
}

选择排序

基本思想:每轮选出最小的元素与每轮第一个元素交换
{5,4,1,3} -> {1,4,5,3} -> {1,3,5,4} -> {1,3,4,5}
平均、最好、最坏的时间复杂度都是o(n^2),空间复杂度o(1)。选择排序不是稳定的如{5,5,2}。

public static void choiceSort(int[] list) {
    int i, j;
    int length = list.length;
    int temp;//记录每一轮最小值
    int index;//记录每一轮最小值下标
    for(i=0;i<length-1;i++){
        temp=list[i];
        index=i;
        for(j=i+1;j<length;j++){
            if(list[j]<temp){
                //更新最小值
                temp=list[j];
                index=j;
            }
        }
        //将最小值与该轮的第一个元素交换
        swap(list,i,index);
    }
}

插入排序

基本思想:玩扑克牌,每次抓取一张牌,都将它插入到手牌合适的位置。
原数组:{5,4,1,3}
排序:{5} -> {4,5} -> {1,4,5} -> {1,3,4,5}
最好情况时间复杂度:o(n),如{1,2,3,4},只需要遍历(n-1)次。平均、最坏均是o(n^2)。空间复杂度:o(1)。插入排序是稳定的。

public static void insertSort(int[] list) {
    int i, j;
    int length = list.length;
    int temp;//记录抽取卡牌的值
    //从第二张牌开始插入
    for(i=1;i<length;i++){
        temp=list[i];
        //将抽取的牌与手牌逐一比较,找到合适位置,大于抽取卡牌值的手牌需要向后移一步
        for(j=i;j>0&&temp<list[j-1];j--){
            list[j]=list[j-1];
        }
        list[j]=temp;//插入合适位置
    }
}

希尔排序

基本思想:其实希尔排序是插入排序的一种优化,插入排序中提及到,当所需要排序的数组是有序的(默认为升序)情况下,达到最优时间复杂度。希尔排序就是借助这个特性进行优化,设置一个增量值gap,先粗略地对数组进行插入排序,通过不断缩小gap的值,继续对数组进行细化排序,由于每次都是基于前一次排序的结果进行插入排序,使得比较次数减小。借用dreamcatcher-cx博主的图:
这里写图片描述

public static void shellSort(int[] list) {
    int i, j;
    int length = list.length;
    int temp;//插入的卡牌
    int gap;//增量值
    for(gap=length/2;gap>0;gap/=2){
        for(i=gap;i<length;i++){
            //流程和插入排序差不多
            temp=list[i];
            //注意循环出口j-gap>=0,否则在判断temp<list[j-gap]会报数组越界错误
            for(j=i;j-gap>=0&&temp<list[j-gap];j-=gap){
                list[j]=list[j-gap];
            }
            list[j]=temp;
        }
    }
}

值得注意的是,希尔排序并不像直接插入排序,它是不稳定的,例如{4,3,3,2},当gap=2,排序得:{3,2,4,3};当gap=1,排序得{2,3,3,4}。另外,希尔排序的平均时间复杂度也是不确定的o(nlogn)~o(n^2),最好情况o(n^1.3),最坏情况o(n^2)。空间复杂度o(1)。


快速排序

基本思想:【坐在马桶上看算法】算法3:最常用的排序——快速排序
时间复杂度:平均o(nlogn),最优o(nlogn),最坏o(n^2)如{4,3,2,1}
空间复杂度:o(logn)~o(n),主要因为使用递归会占用栈空间
稳定性:不稳定

public static void sort(int[] list,int left,int right) {
if(left>=right) return;//递归出口
    //找到中点
    int mid=execute(list,left,right);
    //左右分支
    sort(list,left,mid-1);
    sort(list,mid+1,right);
}

public static int execute(int[] list,int left,int right){
    int base=list[left];//设置基数,这里取最左元素,会可能导致出现最坏情况,后续优化
    int index=left;//记录基数下标
    while(left<right){
        //右指针向左移动,找到比基数值小的元素(为什么右指针先行?)
        while(left<right){
            if(list[right]<base) break;
            right--;
        }
        //左指针向右移动,找到比基数大的元素
        while(left<right){
            if(list[left]>base) break;
            left++;
        }
        if(left<right){
            swap(list,left,right);//左右指针值交换
        }
    }
    swap(list,index,left);//基数与中间值交换
    return left;//返回中间值下标
}

这里有两个问题,首先解决为什么右指针先移动,我们假设现在处于这种情况:
quick1
选取5为基数
quick2
如果左指针先行,找到7>5
quick3
然后右指针向左移动,right==left,跳出循环,执行swap(list,index,left)。
quick4
将基数和中间值交换后,发现基数5的左边明显不是所有数都小于5,排序出现错误。

那么第二个问题,为什么选取最左或者最右元素作为基数会可能导致最坏情况出现?继续举个栗子:
快排的基本步骤

if(left>=right) return;//递归出口
//找到中点
int mid=execute(list,left,right);
//右分支
sort(list,mid+1,right);
//左分支
sort(list,left,mid-1);

假设初始数组{4,3,2,1}

这里写图片描述
第一次mid返回3,进入右分支sort(list,4,3)left>right退出递归,进入左分支sort(list,0,2)

这里写图片描述
第二次mid返回0,进入右分支sort(list,1,2),进入左分支sort(list,0,-1)left < right退出递归

这里写图片描述
第三次mid返回2,进入右分支sort(list,3,2)退出递归,进入左分支sort(list,1,1)退出递归。第三次结束后数组排序完成,观察发现,总共执行n-1次。由于每次调用递归,栈深度加一,可以得出空间复杂度o(n),而每次左指针需要遍历到最右边或者右指针需要遍历到最左边,所以时间复杂度为o(n^2)。对于进一步的优化,可参考快速排序及优化(Java实现)


归并排序

基本思想:递归分治,对数组不断地对半拆分,然后再重新排序合并,如图(图片来源
这里写图片描述
时间复杂度:平均、最好、最坏都是o(nlogn)。
空间复杂度:o(n),借助临时数组。
稳定性:稳定。

public static void mergeSort(int[] list,int left,int right) {
    if(left>=right) return;//递归出口
    //找到中点
    int mid=(left+right)/2;
    //左右分支
    mergeSort(list,left,mid);
    mergeSort(list,mid+1,right);
    //归并
    execute(list,mid,left,right );
}

public static void execute(int[] list,int mid,int left,int right){
    //临时数组
    int[] temp=new int[right-left+1];
    int i=left,j=mid+1;
    int k=0;
    while(i<=mid&&j<=right){
        //左右两边比较,较小的先放入数组
        if(list[i]<list[j]){
            temp[k++]=list[i++];
        }else{
            temp[k++]=list[j++];
        }
    }
    //将剩余的放入temp
    while(i<=mid) temp[k++]=list[i++];
    while(j<=right) temp[k++]=list[j++];
    //将临时数组值放入list
    for(int t=0;t<temp.length;t++){
        //注意放入list的起始下标应该从left开始
        list[left+t]=temp[t];
    }
}

堆排序

基本思想:构建一个堆, 使得根节点值最大,将根节点与堆最后一个元素交换,重新构建堆,堆大小减一,反复执行。
时间复杂度:o(nlogn)。
空间复杂度:o(1)。
稳定性:不稳定。

public static void heapSort(int[] list) {
    //建堆,从最后一个父节点开始,由下至上构建堆
    for(int i=list.length/2-1;i>=0;i--){
        adjust(list,list.length,i);
    }
    //排序
    for(int i=list.length-1;i>0;i--){
        //堆顶和数组最后一个元素交换
        swap(list,0,i);
        //调整堆
        adjust(list,i,0);
    }
}

/**
 * 构建或者调整堆
 * @param list
 * @param size 堆大小
 * @param i 当前父节点
 */
public static void adjust(int[] list,int size,int i){
    //左右节点
    int left=i*2+1;
    int right=i*2+2;
    //记录左右节点最小值下标
    int maxindex=i;
    if(left<size&&list[left]>list[maxindex]) maxindex=left;
    if(right<size&&list[right]>list[maxindex]) maxindex=right;
    //minIndex未发生改变,即当前父节点最小,返回
    if(maxindex==i) return;
    //交换父子节点,重新构建堆
    swap(list,i,maxindex);
    adjust(list,size,maxindex);
}
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值