数据结构(算法篇)

本文通过百鸡百钱问题展示了三种不同的算法解决方案,强调了算法的效率与数学基础的重要性。通过对比三层循环、两层循环和一层循环,直观演示了循环嵌套减少带来的性能提升。同时讲解了时间复杂度和空间复杂度的概念,并给出了排序算法优化实例,如冒泡排序与插入排序的性能对比。
摘要由CSDN通过智能技术生成

数据结构算法篇

算法的魅力

使用经典的百鸡百钱问题来说明,一共列举了三种方式,每种方式的循环层数都不一样

//三层循环
long start1 = System.nanoTime();
for (int i = 0; i < 20; i++) {
    for (int j = 0; j < 34; j++) {
        for (int k = 0; k < 300; k = k + 3) { //k = k + 3 是为了满足小鸡实际存在的要求
            if (5 * i + 3 * j + k / 3 == 100 && i + j + k == 100) {
                System.out.println(i + "-" + j + "-" + k);
            }
        }
    }
}
long end1 = System.nanoTime();
System.out.println("三层循环耗时:" + (end1 - start1));

//两层循环
long start2 = System.nanoTime();
int k_temp = 0, k = 0;
for (int i = 0; i < 20; i++) {
    for (int j = 0; j < 34; j++) {
        k_temp = 100 - i - j;
        if (k_temp % 3 == 0) {
            k = k_temp;
        }
        if (5 * i + 3 * j + k / 3 == 100 && i + j + k == 100) {
            System.out.println(i + "-" + j + "-" + k);
        }
    }
}
long end2 = System.nanoTime();
System.out.println("两层循环耗时:" + (end2 - start2));

/**
         * 一层循环  运用到了数学里面的代替法则,所以说程序最尖端的技术是算法,算法最基本的素养是数学
             * 第一步:x,y与z的关系
             * 5x + 3y + (100 - x - y)/3 = 100 --->y = 25 - 2x + x/4
             *
             * 第二步:替代变量t,演变为
             * t = x/4;
             * y = 25-2x+t
             *
             * 第三步:得到x,y,z与替代变量关系
             * y = 25-2*4t + t ==> y = 25-7t
             * x = 4t
             * z = 100 - 25 + 7t -4t = 75+3t;
             *
             * 第四步:得到t的最大值,要保证x,y,z都要大于等于0,所以 t为25/7 取整得3
             * x = 4t                           -->0 = 4*t --->t = 0 没任何意义
             * y = 25-7t                        -->0 = 25-7t --->t = 25/7
             * z = 100 - 25 + 7t -4t = 75+3t;   -->0 =75+3t --->t = -25 没意义
             *
             * 第五步:判断依据(5 * x + 3 * y + z / 3) == 100 && (x + y + z) == 100
         */
long start3 = System.nanoTime();
int x, y, z, t;
for (t = 0; t <= 3; t++) {
    x = 4 * t;
    y = 25 - 7 * t;
    z = 75 + 3 * t;
    if ((5 * x + 3 * y + z / 3) == 100 && (x + y + z) == 100) {
        System.out.println(x + "-" + y + "-" + z);
    }
}
long end3 = System.nanoTime();
System.out.println("一层循环耗时:" + (end3 - start3));

#输出结果
    0-25-75
    4-18-78
    8-11-81
    12-4-84
    三层循环耗时:23438800
    0-25-75
    4-18-78
    8-11-81
    12-4-84
    两层循环耗时:432300
    0-25-75
    4-18-78
    8-11-81
    12-4-84
    一层循环耗时:280700

结论:很明显,循环嵌套层越少,效率越高

算法概念

时间复杂度

时间复杂度是对一个算法在运行过程中计算耗时的一个量度,也是优化算法的最核心的目标依据。

空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,这个概念在很早以前是需要关注的,但是随着硬件设施的提升和廉价,空间复杂度逐渐变成了边角料,我们只需要在平常的程序设计中尽量注意一些习惯就好:

1、尽量用一维数组,二维数组用结构体一维数组代替
2、尽量少引入中间状态、临时状态,减少状态维护占用
3、数组使用尽量注意小初始化,高扩容频率(这个影响到了性能,慎用)
.....

常见的数据结构

  • **栈(Stack):**线性表。类似于弹匣,只能从一个入口压栈、弹栈—先进后出。
  • **队列(Queue):**线性表。只允许在表的一端进行插入操作,而在另一端进行删除操作—先进先出。
  • **数组(Array):**数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。
  • **链表(Linked List):**物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
  • **堆(Heap):**一种特殊的树形数据结构,一般讨论的堆都是二叉树。
  • **散列表(Hash table):**散列表源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。

排序优化

准备工作

定义一个随机数生成方法:

/**
 * 生成随机数组
 *
 * @param len
 * @param max
 * @return
 */
public static Integer[] generateRandomArray(int len, int max) {
    Integer[] arr = new Integer[len];
    for (int i = 0; i < arr.length; i++) {
        arr[i] = (int) (Math.random() * max);
    }
    return arr;
}

测试遍历方法:

public static void main(String[] args) {
    int N = 20000;
    Integer[] arr = generateRandomArray(N, 100000);
    long start = System.currentTimeMillis();
    sort(arr);
    long end = System.currentTimeMillis();
    System.out.println("**排序耗时:" + (end - start));
    System.out.println(Arrays.asList(arr));
}

元素位置交换方法:

	/**
     * 元素位置交换
     *
     * @param arr
     * @param i
     * @param j
     */
    private static void swap(Object[] arr, int i, int j) {
        Object t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

冒泡排序

依次比较相邻的两个数,将小数放在前面,大数放在后面。

第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。至此第一趟结束,将最大的数放到了最后。

第二趟:仍从第一对数开始比较(因为可能由于第2个数和第3个数的交换,使得第1个数不再小于第2个数),将小数放前,大数放后,一直比较到倒数第二个数(倒数第一的位置上已经是最大的),第二趟结束,在倒数第二的位置上得到一个新的最大数(其实在整个数列中是第二大的数)。

如此下去,重复以上过程,直至最终完成排序。

/**
 * 核心代码---双层循环比对
 *
 * @param arr
 */
public static void sort(Integer[] arr) {
    int count = 0;
    for (int i = 0; i < arr.length - 1; i++) {//冒泡趟数
        for (int j = 0; j < arr.length - 1 - i; j++) {
            count++;
            if (arr[j + 1] < arr[j]) {
                swap(arr, j + 1, j);
            }
        }
    }
    System.out.println("循环计算次数:" + count);
}

#输出结果(无论你执行多少次,循环次数都是固定的)
    循环计算次数:199990000
	冒泡排序耗时:1529

结论:两层循环,性能不太理想

冒泡排序优化

改进方法:如果我们发现在排序过程中,没有发生一次交换,我们可以让它提前结束!

我们在排序过程中设置一个标标记flag判断一下元素是否进行交换

	/**
     * 核心代码---双层循环比对
     *
     * @param arr
     */
    public static void sort(Integer[] arr) {
        int count = 0;
        //标记变量,表示是否进行过交换,如果没有,说明没必要继续往后面比较了
        boolean flag = false;
        for (int i = 0; i < arr.length - 1; i++) {//冒泡趟数
            for (int j = 0; j < arr.length - 1 - i; j++) {
                count++;
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                    flag = true;
                }
                if (!flag) {
                    //没有发生交换则退出循环;
                    break;
                }
            }
        }
        System.out.println("循环计算次数:" + count);
    }

#输出结果(因为随机顺序的不确定性,多执行几次)
    1、循环计算次数:199990000
	   冒泡排序耗时:1654
    2、循环计算次数:19999
	   冒泡排序耗时:14

结论:如果生成的随机数组无序性非常大,执行的计算次数越多,最多时等于普通冒泡排序计算次数;如果是数组是有序的,则可以大大节省时间!

插入排序

插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较 N-1 次,时间复杂度为 O(N)。最坏的情况是待排序数组是逆序的,此时需要比较次数最多,最坏的情况是 O(n^2)

	/**
     * 核心代码---双层循环比对
     *
     * @param arr
     */
    public static void sort(Integer[] arr) {
        int count = 0;
        for (int i = 0; i < arr.length; i++) {
            // 寻找元素 arr[i] 合适的插入位置
            for (int j = i; j > 0; j--){
                count++;
                if (arr[j].compareTo(arr[j - 1]) < 0){
                    swap(arr, j, j - 1);
                }else{
                    break;
                }
            }

        }
        System.out.println("循环计算次数:" + count);
    }

#输出结果
    循环计算次数:100196062
	插入排序耗时:591

结论:循环计算次数相较于普通冒泡排序少了将近一半,原因就是它对于已经排好序的部分不会再次循环,而是将一个记录插入到已经排好序的有序表中,相当于变成了一个记录数增 1 的有序表

希尔排序

希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。

/**
 * 核心代码---循环比对
 *
 * @param arr
 */
public static void sort(Integer[] arr) {
    int count = 0;
    int j;
    for (int gap = arr.length / 2; gap >  0; gap = gap / 2) {
        for (int i = gap; i < arr.length; i++) {
            Integer tmp = arr[i];
            for (j = i; j >= gap && tmp.compareTo(arr[j - gap]) < 0; j = j -gap) {
                count ++;
                arr[j] = arr[j - gap];
            }
            arr[j] = tmp;
        }
    }
    System.out.println("循环计算次数:" + count);
}

#输出结果
    循环计算次数:361088
	希尔排序耗时:32

结论:比以上任何排序都快,原因就在于通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值