算法系列之入门

算法是结局问题整理的一系列的步骤。本文将整理一些常见的算法,如冒泡排序算法、桶排序算法、插入排序算法等一些比较基础且常见的算法,包括对算法的概念整理以及编码,完整代码在码云上开源,欢迎访问。欢迎大家在阅读指正。


前言

阅读本文的前提是有一定的JAVA基础,并且对算法有一定的认知。


一、冒泡排序

概念

冒泡排序是一种最简单的排序算法,它通过嵌套两次扫描整个数组做到排序。他重复的扫描的两个要比较的元素,不断的调换位置,直到没有需要交换的元素。
示例:

        /**
         * 第一次循环,0~n-1
         * 第二次循环,1~n-1
         * 第三次循环,2~n-1
         * ...
         */
        for (int i = 0; i < arr.length - 1; i++) {
    
    
            /**
             * 第一次循环,i=0,开始将0位置上的数值和1~n上的位置的数据挨个比较一次大小,并交换,
             * 第二次循环,i=1,开始将1位置上的数值和2~n上的位置的数据挨个比较一次大小,并交换,
             * 第三次循环,i=2,开始将2位置上的数值和3~n上的位置的数据挨个比较一次大小,并交换,
             * ...
             */
            for (int j = i + 1; j < arr.length; j++) {
    
    
              ………
            }
        }

完整编码地址: https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/bubblesort

算法分析

算法稳定性

冒泡排序算法,如果遇到两个元素是相等的,那么不会交换元素的位置,所以说,冒泡排序算法的一种***稳定***的排序算法。

时间复杂度

  • 若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数 和记录移动次数 均达到最小值。所以,冒泡排序最好的时间复杂度为 O(N)。
  • 若初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,
    比较和移动次数均达到最大值。所以,冒泡排序最坏的时间复杂度为 O(N^2)。

综上,因此冒泡排序总的平均时间复杂度为O(N^2)。

二、选择排序

概念

选择排序利用双层循环进行,外层第一次循环认为第一个索引位置(0)上的数据是最小的,然后二层循环从1~n-1索引位置上再找出一个比索引0位置上更小的,然后将这个数据进行交换位置存放,一次类推。

示例:

/**
 * 第一次循环,0~n-1
 * 第二次循环,1~n-1
 * 第三次循环,2~n-1
 * ...
 */
for (int i = 0; i < arr.length - 1; i++) {
    
    
    /**
     * 外层第一次循环,认为最小值的下标为i(0),那么应该从下标为1~n个数中找最小值的下标。
     * 外层第二次循环,认为最小值的下标为i(1),那么应该从下标为2~n个数中找最小值的下标。
     * 外层第三次循环,认为最小值的下标为i(2),那么应该从下标为3~n个数中找最小值的下标。
     * ...
     */
    int minIndex = i;
    for (int j = i + 1; j < arr.length; j++) {
    
    
        /**
         * 从下标为i+1~n个数中找最小值的下标,如果找到了比认为的最小值还小的值,那么进行交换
         */
        minIndex = arr[j] < arr[minIndex] ? j : minIndex;
    }
    /**
     * 交换
     */
    swap(i, minIndex, arr);
}

完整编码地址: https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/selectionsort

算法分析

算法稳定性

选择排序算法,每一趟循环都会找第一最小的,第二最小的,如果第一最小的确定后,发现后边的元素中有相等的数据,但是相等的数据又不是第二最小的,那么这一趟就会将第一最小的数据和第二最小的数据进行交换位置,很明显这样会破坏两个相同数据的位置,比如(5 8 5 2 9),所以说,选择排序算法的一种***不稳定***的排序算法。

时间复杂度

  • 选择排序的交换操作介于 0 和 (n - 1)次之间。选择排序的比较操作为 n (n - 1) / 2 次之间。选择排序的赋值操作介于 0 和 3 (n - 1) 次之间。比较次数O(n^2),最好情况是,已经有序,交换0次;最坏情况交换n-1次,逆序交换n/2次。所以,插入排序最好的时间复杂度为 O(n^2)。交换次数比冒泡排序少多了,所以选择排序比冒泡排序快。

综上,因此选择排序总的平均时间复杂度为O(n^2)。

三、 快速排序

概念:

快速排序是对冒泡排序的一种改进,通过多次比较和交换来实现排序。快速排序算法中借助递归来实现。调优技巧,当数据量比较小的时候,可以直接调用插入排序的方式做。

示例:

// 找出一个分界点,进行数组分割。
int point = doQuickSort(arr, left, right);
// 分界点左半部分进行排序
quickSort(arr, left, point - 1);
// 分界点右半部分进行排序
quickSort(arr, point + 1, right);

完整编码地址: https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/quicksort

算法分析

算法稳定性

快速排序算法,多个相同的值的相对位置也许会在算法结束时产生变动,所以说,选择排序算法的一种***不稳定***的排序算法。

时间复杂度

快速排序算法的时间复杂度与划分的趟数有关。

  • 最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)。
  • 理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。

综上,因此快速排序总的平均时间复杂度为O(n2),改善情况下可以达到O(nlog2n)。

空间复杂度

S(n) = O(log2n)

四、 归并排序

概念

归并排序是一种稳定的排序算法,算法采用分治的思想,将一个大序列分别分割成几个小序列,然后对子序列进行排序,最后将所有已经排好序的子序列合并,完成归并排序。若将两个有序表合并成一个有序表,称为二路归并。

示例:

// 找出分治的中间点
// int mid = left + ((right -left) >> 1);
int mid = (left + right) / 2;
// 递归划分右边的区域
mergeSort(arr, left, mid, help);
// 递归划分左边的区域
mergeSort(arr, mid + 1, right, help);
// 合并已经排序完毕的部分
merge(arr, left, mid, right, help);

完整编码地址: https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/mergesort

算法分析

算法稳定性

归并排序算法,如果遇到两个元素是相等的,那么不会交换元素的位置,所以说,归并排序算法的一种***稳定***的排序算法。

时间复杂度

  • 普通归并排序在排序是将序列不断的一分为二,并将分割后的序列进行排序,所以时间复杂度为O(nlogn)。
  • 改进归并排序在归并时先判断前段序列的最大值与后段序列最小值的关系再确定是否进行复制比较。如果前段序列的最大值小于等于后段序列最小值,则说明序列可以直接形成一段有序序列不需要再归并,反之则需要。所以在序列本身有序的情况下时间复杂度可以降至O(n)。

综上,因此归并排序总的平均时间复杂度为O(nlogn)。

空间复杂度

S(N) = O(n)

五、 插入排序

概念

插入排序的思想是将一个数据插入一个已经排好序的数组中,实现过程使用双层循环,第一层循环的所有元素都进行一遍插入,内层循环
控制这个外层数据的位置。

示例:

               /**
                * 第一次循环,0~n-1
                * 第二次循环,1~n-1
                * 第三次循环,2~n-1
                * ...
                */
               for (int i = 0; i < arr.length; i++) {
    
    
                   /**
                    * 第一次循环,i=0,开始比较-1~0上的位置的数据大小,并交换,
                    * 第二次循环,i=1,开始比较0~1上的位置的数据大小,并交换,
                    * 第三次循环,i=2,开始比较1~2上的位置的数据大小,并交换,
                    * ...
                    */
                   for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
    
    
                       swap(j, j + 1, arr);
                   }
               }

完整编码地址:https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/insertsort

算法分析

算法稳定性

插入排序算法,如果遇到两个元素是相等的,那么不会交换元素的位置,所以说,插入排序算法的一种***稳定***的排序算法。

时间复杂度

  • 若数组的初始状态是有序的,那么只需要循环一遍数组,且每个数据只需要和剩余的n-1个数据进行一次比较即可,此时均达到最小值。所以,插入排序最好的时间复杂度为 O(N)。
  • 若数组的初始状态不是有序的,需要进行n-1趟排序。每趟排序要进行n-1次比较,且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,
    比较和移动次数均达到最大值。所以,插入排序最坏的时间复杂度为 O(N^2)。

综上,因此插入排序总的平均时间复杂度为O(N^2)。

空间复杂度

S(N) = O(1)

六、 桶排序

概念

桶排序就是将一个数组分成好几部分,并将这几个部分分别放进
不同的桶中,然后每个桶中再进行局部排序(可能会使用别的排序算法)。最后将所有的桶中的数据全部一次倒出,合并后,便能做到整个数组有序。

完整编码地址:https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/bucketsort

基数排序

根据原始数据,创建和原始数据大小相等的桶进行辅助排序。然后找出原始数组中最大的数据的位数,使其他剩余的数据的位数和最大位数对其,然后对所有的数据,以个位、十位、百位……上的数作为索引,分别放进对应索引位置上的辅助的桶中,直到最后,数组即可做到有序。

示例:

        // 创建一个大小为10的数组作为桶
        ArrayList<ArrayDeque<String>> bucket = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
    
    
            // 桶中每个位置上存放一个和原数组等长的队列
            ArrayDeque<String> da = new ArrayDeque<>(arr.length);
            bucket.add(da);
        }
	
		// 原数组中其他数据不够最大数据的位数时,前边补充0补足位数。
        for (int i = 0; i < arr.length; i++) {
    
    
            String data = String.valueOf(arr[i]);
            if (data.length() < maxLeg) {
    
    
                data = num.format(arr[i]);
            }
            str[i] = data;
        }

        /**
         * 根据最大位数,对每一个数据进行分桶
         * 比如:第一次循环,个位为1的数据存放进下标为1的桶的队列中…………
         *      第二次循环,十位为1的数据存放进下标为1的桶的队列中…………
         *      …………
         */
        for (int i1 = maxLeg - 1; i1 > - 1; i1--) {
    
    
            // 根据最大位数,对每一个数据进行分桶
            for (String s : str) {
    
    
                int da = Integer.parseInt(String.valueOf(s.charAt(i1)));
                bucket.get(da).add(s);
            }

            int index = 0;
            /**
             * 第一次循环,按照个位分桶后对数组还原…………
             * 第二次循环,按照十位分桶后对数组还原…………
             * …………
             */
            for (ArrayDeque<String> arrayDeque : bucket) {
    
    
                while (!arrayDeque.isEmpty()) {
    
    
                    str[index++] = arrayDeque.poll();
                }
            }
        }

算法分析

算法稳定性

基数排序算法是一种***稳定***的排序算法。

时间复杂度

基数排序算法的时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数。

空间复杂度

基数排序算法的空间复杂度是相当高的,当数据越多,需要的桶也就越多。

计数排序

计数排序是一种非比较排序。基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。

计数排序算法示例:

{
    
    
    count = new int[10];
    for (String s : help) {
    
    
        int index = Integer.parseInt(String.valueOf(s.charAt(i)));
        count[index]++;
    }

    // 将处理好的count进行累加.形成新的count
    for (int i1 = 1; i1 < count.length; i1++) {
    
    
        count[i1] = count[i1] + count[i1 - 1];
    }

    /**
     * 然后数据应该按照从右到左的顺序循环
     * 例如:最后一个位置上的数据A,个位数为2,那么找到count中下标为2的位置上的数据,假如为5,那么
     * A应该放在数组下标为4的位置上,并且count中下标为2的位置上的数据减一,就变成了4,下一个位置上的数据同理。
     */
    for (int i1 = help.length - 1; i1 >= 0; i1--) {
    
    
        int index = Integer.parseInt(String.valueOf(help[i1].charAt(i)));
        help1[--count[index]] = help[i1];
    }
    // 每次处理过一遍的数组参与下一次处理流程,最终数组有序
    help = Arrays.copyOf(help1, help1.length);
}

算法分析

算法稳定性

计数排序算法是一种***稳定***的排序算法。

时间复杂度

计数排序算法一定程序上时间复杂度为O(n)。

七、 堆结构

概念

堆可以看做是一个树形结构的数组,堆有以下两个特点:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

算法如下:

假设在一个小跟堆中,将元素放入堆中之后,那么根据该元素所在的索引,能知道起父亲节点的索引位置为:root = (i - 1) / 2,左
孩子的节点的索引位置为:left = 2 * i,右孩子节点的索引位置为:right = 2 * i + 1。
那么小跟堆逻辑调整为:如果当前节点的父亲节点大于当前节点,当前节点需要来到父亲节点的位置,父亲节点来到当前节点的位置,当前索引位置变成父亲节点的索引。

示例:

                // 小根堆逻辑调整,如果当前节点的父亲节点大于当前节点,当前节点需要来到父亲节点的位置,父亲节点来到当前节点的位置。
                while (heap[(index - 1) / 2] > heap[index]) {
    
    
                    // 调整,当前节点的父亲节点大于当前节点,当前节点需要来到父亲节点的位置,父亲节点来到当前节点的位置
                    swap(heap, (index - 1) / 2, index);
                    // 当前节点的索引来到其父亲节点的索引位置。
                    index = (index - 1) / 2;
                }

完整编码地址:https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/heapsort

算法分析

时间复杂度

  • 初始建堆的时间复杂度为:O(n)。
  • 更改堆元素后重新调整堆元素的时间复杂度为:O(logn)。

综上,因此冒泡排序总的平均时间复杂度为T(n) = O(nlogn)。

堆排序

概念

不管是大根堆还是小根堆,最顶层的数据节点一定是全局最大的或者最小的节点,那么可以利用这个特点,进行堆排序,
将调整的堆的第一个元素和堆中的最后一个元素进行交换,并且堆的大小减一,之后再重新进行堆调整,直到最后一个元素
为止,这样就可以做到排序。
示例:

        /**
         * 堆排序:
         * 1、对于大根堆来说,最顶端位置上的数据就是整个堆中最大的数据,利用这个特点,
         * 每次将这个最顶端的数据和堆最后边位置上的数据交换后,堆的小大减1,并且将0至减1后的
         * 堆上的数据重新进行大根堆的调整,调整后继续之前的操作,直到交换的位置到达0为止,也就是
         * 堆大小一直减1减到0为止。此时,堆中的数据就变成了有序的数,而且是升序的。
         * 2、对于小根堆来说,最顶端位置上的数据就是整个堆中最小的数据,利用这个特点,
         * 每次将这个最顶端的数据和堆最后边位置上的数据交换后,堆的小大减1,并且将0至减1后的
         * 堆上的数据重新进行小根堆的调整,调整后继续之前的操作,直到交换的位置到达0为止,也就是
         * 堆大小一直减1减到0为止。此时,堆中的数据就变成了有序的数,而且是降序的。
         */
        public void sorted () {
    
    
            if (heap.length == 0) {
    
    
                throw new IllegalStateException("Heap Size is empty");
            }

            // 堆的大小
            int sortedIndex = heapSize;
            // 堆的大小没到0的时候,一直进行交换,堆调整操作
            while (sortedIndex > 0) {
    
    
                // 将0位置上的数据与最后位置上的数据进行交换,并且堆大小减1
                swap(heap, 0, --sortedIndex);
                // 交换后,进行堆调整
                afterSortefHeapfy(heap, 0, sortedIndex);
            }
        }

算法分析

时间复杂度

时间复杂度为T(n) = O(nlogn)。

空间复杂度

S(N)=O(1)

八、 链表

概念

链表是一组非连续的,非顺序的存储结构,在逻辑上通过链表的指针做到依次链接。链表由一系列的节点组成,每一个节点都有一个指向后一个节点的指针,通过这个指针可以找到下一个节点,如果节点指向后一个节点的指针,也有指向前一个节点的指针,那么组成的链表是单向链表,如果节点有两个指针,那么组成的链表叫做双向链表。
示例:

        // 单向链表,只有指向下一节点的指针
        singleNode1.next = singleNode2;
        
        // 双向链表,有指向下一节点的指针,也有指向上个节点的指针
        DoubleNode2.pre = DoubleNode1;
        DoubleNode2.next = DoubleNode3;

完整编码地址:https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/linkedlist

九、栈和队列

  • 队列:先进先出
  • 栈:先进后出

完整编码地址:https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/stackqueue

使用栈结构实现队列

原理:

使用两个栈结构实现,第一个栈在添加数据时进行数据保存,第二个栈在取数据从第一个栈中倒序获取数据,然后存放进去,
这样操作后,第一个栈先进的数据存放在了第二个栈的栈顶,弹出时,先弹出栈顶数据,相当于队列的先进先出。
示例:

		public synchronized Integer poll() {
    
    
            if (stack2.isEmpty()) {
    
    
                // 第二个栈用来倒序保存第一个栈中的数据,利用栈的先进后出的特点,实现队列。
                for (int i = stack1.size() - 1; i >= 0; i--) {
    
    
                    stack2.add(stack1.pop());
                }
            }
            // 第一个栈先进的数据存放在了栈顶,弹出时,先弹出栈顶数据,相当于队列的先进先出
            return stack2.pop();
        }

使用队列结构实现栈

原理:

使用两个队列实现,第一个队列用来存储原始数据,第二个队列用来存储第一个队列除了最后一个进来的那个数据的其他的数据,
此时,第一个队列就剩余了一个数据,也就是最后进来的那个数据,弹出时,也就是最后进队列的数据,先弹出。做到了栈结构的
先进后出,后进先出。
示例

		@Override
        public Integer pop() {
    
    
            // 将第一个队列中除了最后进来的那个数据之外,其他的数据都备份到第二个队列中
            while (queue1.size() > 1) {
    
    
                queue2.add(queue1.poll());
            }
            // 此时,第一个队列就剩余了一个数据,也就是最后进来的那个数据,
            Integer data = queue1.poll();
            // 第一个队列弹出数据后,将剩余的数据还原
            queue1.addAll(queue2);
            queue2.clear();
            return data;
        }

十、 二分法查找

概念

二分法查找是一种折半查找算法。在数据量比较大的情况下,利用二分法查找数据效率会比较高,但是二分法的前提是,数组必须有序。
示例:

        int left = 0; // 数组最小下标
        int right = arr.length - 1; // 数组最大下标
        int middle; // 中间值下标
        while (left <= right) {
    
    
            /**
             * 中间值下标计算方法:为避免left+right内存溢出,利用left + ((right - left) / 2)代替,
             * 以肯定程度上可以避免溢出问题。
             * middle = left + ((right - left) / 2) <=> middle = left + ((right - left) >> 2);
             */
            middle = left + ((right - left) / 2);
            if (arr[middle] > find) {
    
     // 如果中间值比要找的值大,说明要找的值在中间值的左侧
                right = middle - 1;
            } else if (arr[middle] < find) {
    
     // 如果中间值比要找的值小,说明要找的值在中间值的右侧
                left = middle + 1;
            } else {
    
    
                return middle; // 如果中间值和要找的值相等,说明要找的值就是该下标对应的值
            }
        }

完整编码地址:https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/dichotomy

算法如下:

  • 1.确定查找范围left=0,right=N-1,计算中项mid = left + ((right - left) / 2)。
  • 2.若a[mid]=x或left>=right,则结束查找;否则,向下继续。
  • 3.若a[mid]<x,说明待查找的元素值只可能在比中项元素大的范围内,则把mid+1的值赋给right,并重新计算mid,转去执行步骤2;若a[mid]>x,
    说明待查找的元素值只可能在比中项元素小的范围内,则把mid-1的值赋给left,并重新计算mid,转去执行步骤2。

算法分析

时间复杂度

  • 若查找数组刚好是奇数个,刚好要找的数在中间的位置,那么只需要一次二分即可查找到。所以,二分法查找最好的时间复杂度为 O(1)。
  • 若要查找的数据刚好在数组的第一个位置或者最后一个位置,那么至少要二分n/2+1次。所以,冒泡排序最坏的时间复杂度为 O(logn)。

综上,因此二分法查找总的平均时间复杂度为T(N) = O(logn)。

空间复杂度

S(n)=logn

十一、 异或(xor)实现

概念

异或也叫半加运算,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假,则异或的运算法则为:00=0,10=1,01=1,11=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。异或略称为XOR。

算法法则:

  1. 归零律

    a ^ a = 0

  2. 结合律

    a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c

  3. 恒等律

    a ^ 0 = a

  4. 交换律

    a ^ b = b ^ a

  5. 自反律

    a ^ b ^ a = b

只有在两个比较的位不同时其结果是1,否则结果为0,即“两个输入相同时为0,不同则为1”!

完整编码地址:https://gitee.com/huannzi/oj_algorithm/tree/master/src/main/java/exclusiveor

总结

本文将整理一些常见的算法,如冒泡排序算法、桶排序算法、插入排序算法等一些比较基础且常见的算法,包括对算法的概念整理以及编码,完整代码在码云上开源,欢迎访问。欢迎大家在阅读指正。

猜你喜欢

转载自blog.csdn.net/qq_22610595/article/details/122245698