Java数据结构第一讲-排序算法(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Java数据结构第一讲-排序算法(下)
4.1.7、基数排序
  • O(n) 空间复杂度O(rd) 稳定(基数排序必须依赖于另外的排序方法 实质是多关键字排序)
    是通过比较数字将其分配到不同的“桶里”来排序元素的。他是线性排序算法之一。
  • 解决方案:1、最高位优先法MSD 2、最低位优先法LSD
  • 算法步骤
  • 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
  • 从最低位开始,依次进行一次排序
  • 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
  • 基数排序图解如下

参考代码

public class RadixSort implements IArraySort {
     @Override
     public int[] sort(int[] sourceArray) throws Exception {
         // 对 arr 进行拷贝,不改变参数内容
         int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
         int maxDigit = getMaxDigit(arr);
        return radixSort(arr, maxDigit);
    }
    /**
     * 获取最高位数
     */
    private int getMaxDigit(int[] arr) {
        int maxValue = getMaxValue(arr);
        return getNumLenght(maxValue);
    }
    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }
    protected int getNumLenght(long num) {
        if (num == 0) {
            return 1;
        }
        int lenght = 0;
        for (long temp = num; temp != 0; temp /= 10) {
            lenght++;
        }
        return lenght;
    }
    private int[] radixSort(int[] arr, int maxDigit) {
        int mod = 10;
        int dev = 1;
        for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
            // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
            int[][] counter = new int[mod * 2][0];
            for (int j = 0; j < arr.length; j++) {
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }
            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
        }
        return arr;
    }
    private int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }
}
4.1.8、桶排序
  • O(n) 将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序
  • 适用场景:外部排序中(磁盘中,内存有限,无法将数据全部加载到内存中)
  • 算法步骤
  • 设置固定数量的空桶。
  • 把数据放到对应的桶中。
  • 对每个不为空的桶中数据进行排序。
  • 拼接不为空的桶中数据,得到结果
  • 动画演示

4.1.9、计数排序
  • 桶排序的一种特殊形式:每个桶中的数据相同
  • 算法步骤
  • 设置固定数量的空桶。
  • 把数据放到对应的桶中。
  • 对每个不为空的桶中数据进行排序。
  • 拼接不为空的桶中数据,得到结果
  • 基数排序图解如下

4.1.10、排序方法的选择?
  • n代表数据量
  • 1、n较小,可以采用直接插入或直接选择排序
  • 2、若文件初始状态基本有序,应选用直接插入、冒泡或随机的快速排序
  • 3、n较大,采用复杂度为O(nlgn)的方法:快排/堆排/归并
  • 4、在实际的软件开发中,为什么我们更倾向于使用插入排序而不是冒泡排序算法?
  • 从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个,所以在对相同数组进行排序时,冒泡排序的运行时间理论上要长于插入排序。

如果数据存储在链表中,这三种排序算法还能工作吗?

一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;

插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;

选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。

4.2、排序工具类Arrays?如何实现一个通用的、高性能的排序函数?(Java语言采用堆排序实现排序函数,C语言使用快速排序实现排序函数)
  • Arrays拥有一组static方法(equals():比较两个array是否相等/fill():将值填入array中/sort():用来对array进行排序/binarySearch():在排好序的array中寻找元素/system.arraycopy():array的复制)
  • Jdk7中Arrays.sort()和Collections.sort()排序方法使用注意: jdk1.6中的arrays.sort()和 collections.sort()使用的是MergeSort; jdk1.7中内部实现转换成了TimSort方法,
  • 对对象间比较的实现
    1、有两个参数,第一个是比较的数据,第二个是比较的规则,如果comparator为空,则使用comparableTimSort的sort实现
    2、传入的待排序数组若小于MIN_MERGE(Java实现中为32)则从数组开始处找到一组连接升序或严格降序(找到后翻转)的数
    BinarySort:使用二分查找的方法将后续的数插入之前的已排序数组
    3、开始真正的TimSort过程(选取minRun大小,之后待排序数组将被分成以minRun大小为区块的一块块子数组)
    Timsort的思想:找到已经排好序的数据子序列,然后对剩余部分排序,最后合并起来
  • java提供的默认排序算法
    1、对于基础数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序;
    2、而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并(Merge)和二分插入排序(binary Sort)结合的优化排序算法。
    思路是查找数据集中已经排好序的分区(这里叫run 连续升或降的序列),然后合并这些分区来达到排序的目的。
  • Java8引入了并行排序算法(直接使用parallelSort方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于fork-join框架,当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是,当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境.
  • fork-join框架的适用场景:计算密集型,而非IO密集型,踩过坑

4.3、常见的查找算法?
  • 1、二分查找法(考虑好边界条件,不要被面试官抓住漏洞)(使用Arrays工具类的binarySearch方法)
    思路:先确定数组的中间位置,然后将要查询的值与数组中间位置的值进行比较,若小于数组中间值,则要查找的值应位于该中间值之前,依次类推;
    算法: 1、如果关键字小于中央元素,只需继续在数组的前半部分进行搜索;2、如果关键字与中央元素相等,则搜索结束,找到匹配元素;3、如果关键字大于中央元素,只需继续在数组的后半部分进行搜索
    限制:用于顺序链表或排序后的链表
    注意事项:1、循环退出条件low<=high;2、mid的取值(low+(high-low)>>1)因为相比除法运算来说,计算机处理位运算要快得多;3、low和high的更新low=mid+1,high=mid-1
    时间复杂度:O(lgn)
public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid = (low + high) / 2;//或者int mid = low+((high-low)>>1);
    if (a[mid] == value) {
      return mid;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return -1;
}
  • 2、4种常见的二分查找变形问题
    第一种:查找第一个值等于给定值的元素
public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid = low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      if ((mid == 0) || (a[mid - 1] != value)) 
        return mid;  //mid不是第一个数或mid左边的数不是
      else high = mid - 1;
    }
  }
  return -1;
}

第二种:查找最后一个值等于给定值的元素

public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid = low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      if ((mid == n - 1) || (a[mid + 1] != value))
        return mid;
      else
        low = mid + 1;
    }
  }
  return -1;
}

第三种:查找第一个大于等于给定值的元素

public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid = low + ((high - low) >> 1);
    if (a[mid] >= value) {
      if ((mid == 0) || (a[mid - 1] < value))
        return mid;
      else
        high = mid - 1;
    } else {
      low = mid + 1;
    }
  }
  return -1;
}

第四种:查找最后一个小于等于给定值的元素

public int bsearch7(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;
  while (low <= high) {
    int mid = low + ((high - low) >> 1);
    if (a[mid] > value) {
      high = mid - 1;
    } else {
      if ((mid == n - 1) || (a[mid + 1] > value))
        return mid;
      else
        low = mid + 1;
    }
  }
  return -1;
}
  • 3、如果有序数组是一个循环有序数组,比如4,5,6,1,2,3。针对这种情况,如何实现一个求“值等于给定值”的二分查找算法呢?
public int search(int[] nums, int target) {
  if (nums.length == 1 && nums[0] == target)
    return 0;
  int left = 0;
  int right = nums.length - 1;
  int mid = 0;
  while (left < right) {
    mid = (left + right) >> 1;
    if (nums[left] == target)
      return left;
    if (nums[right] == target)
      return right;
    if (nums[mid] == target)
      return mid;
    if (nums[mid] > nums[left]) { // 第一种情况
      if (target > nums[mid]) {
        left = mid + 1;   //在mid到左侧最大值区间
      } else {//target小于中间值
        if (target > nums[left]) {
          right = mid - 1;
        } else {
          left = mid + 1;  //在右侧区间
        }
      }
    } else { // 第二种情况   mid小于最左值
      if (target > nums[mid]) {//两种情况:1、在mid右侧  2、在左侧
        if (target < nums[right]) {
          left = mid + 1; //1、在mid右侧 
        } else {
          right = mid - 1; //2、在左侧
        }
      } else {  //在右侧的左边区域
        right = mid - 1;
      }
    }
  }
  return -1;
}
  • 4、x的平方根 LeetCode69 实现int sqrt(int x)函数。计算并返回x的平方根,其中x是非负整数,由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去
  • 方法1:java自带API
public int mySqrt(int x) {
  return (int)Math.sqrt(x);
}
  • 方法2:二分搜索
int mySqrt(int x) {
  //注:在中间过程计算平方的时候可能出现溢出,所以用long long。
  long i=0;
    long j=x/2+1;//对于一个非负数n,它的平方根不会大于(n/2+1)
  while(i<=j)
  {
    long mid=(i+j)/2;
      long res=mid*mid;
    if(res==x) return mid;
    else if(res<x) i=mid+1;
    else j=mid-1;
  }
  return j;
}

方法3:牛顿迭代法 求c的算术平方根就是求f(x)=x^2-c的正根 迭代公式:xn+1=1/2(xn+c/xn)

int mySqrt(int x) {
  if (x == 0) return 0;
  double last=0;
  double res=1;
  while(res!=last)
  {
    last=res;
    res=(res+x/res)/2;
  }
  return int(res);
}

4.4、复杂度分析

常见的时间复杂度?表示的是一个算法执行效率与数据规模增长的变化趋势

时间复杂度 概念
  1. O(1) 常数阶| 常量级别的时间复杂度:只要代码的执行时间不随n的增大而增长,这样代码的时间复杂度我们都记作O(1)。
    2、O(logn)对数阶、O(nlogn)线性对数阶| 代码循环执行的次数呈现对数关系
    3、O(m+n)、O(m*n) | 代码的复杂度由两个数据的规模来决定

空间复杂度:(表示算法的存储空间与数据规模之间的增长关系)

常见的空间复杂度就是O(1)、O(n)、O(n2)

  • 平均时间复杂度(加权平均时间复杂度):加了概率
  • 均摊时间复杂度:对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。
  • 算法的最好情况和最坏情况?
    最好情况:算法执行最佳的数据排列。如:二分搜索时,目标值正好位于搜索的数据中心,时间复杂度为0;
    最差情况:给定算法的最差输入。如:快速排序中,如果选择的关键值是列表中最大或最小值,最差情况就会发生,时间复杂度会变成O(n^2)

4.5、如何高效地判断无序数组中是否包含某特定值?
// 方法1:使用list  (**最常使用**)
public static boolean useList(String[] arr, String targetValue) {
   return Arrays.asList(arr).contains(targetValue);
}
// 方法2:使用Set  低效
>public static boolean useSet(String[] arr, String targetValue) {
  Set<String> set = new HashSet<String>(Arrays.asList(arr));
  return set.contains(targetValue);
}
// 方法3:使用一个简单循环  最高效
>public static boolean useLoop(String[] arr, String targetValue) {
  for(String s: arr){
    if(s.equals(targetValue))
      return true;
  }
  return false;
}
  • 方法4:Arrays.binarySearch()方法:数组必须是有序的(有序数组时,使用列表或树可达到O(lgn),使用hashset可达到O(1))

4.6、查找算法实战?
  • 1、我们要给电商交易系统中的“订单”排序。订单有两个属性(下单时间,订单金额) 需求是按金额从小到大对订单数据排序。对金额相等的订单,按下单时间从早到晚排序
    稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变
    思路:先按下单时间给订单排序,排完序之后,使用稳定排序算法,按订单金额重新排序(稳定排序算法可以保持金额相同的两个对象,在排序之后的前后顺序不变)
  • 2、O(n)时间复杂度内求无序数组中的第K大元素?(利用分区的思想) 代码放在eclipse中
    我们选择数组区间A[0…n-1]的最后一个元素A[n-1]作为pivot,对数组A[0…n-1]原地分区,这样数组就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1]。
    如果p+1=K,那A[p]就是要求解的元素;如果K>p+1, 说明第K大元素出现在A[p+1…n-1]区间,我们再按照上面的思路递归地在A[p+1…n-1]这个区间内查找。同理,如果K<p+1,那我们就在A[0…p-1]区间查找。
  • 3、现在你有10个接口访问日志文件,每个日志文件大小约300MB,每个文件里的日志都是按照时间戳从小到大排序的。你希望将这10个较小的日志文件,合并为1个日志文件,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述排序任务的机器内存只有1GB,你有什么好的解决思路
    answer:先构建十条io流,分别指向十个文件,每条io流读取对应文件的第一条数据,然后比较时间戳,选择出时间戳最小的那条数据,将其写入一个新的文件,然后指向该时间戳的io流读取下一行数据,然后继续刚才的操作,比较选出最小的时间戳数据,写入新文件,io流读取下一行数据,以此类推,完成文件的合并, 这种处理方式,日志文件有n个数据就要比较n次,每次比较选出一条数据来写入,时间复杂度是O(n),空间复杂度是O(1),几乎不占用内存。

Action1:

Action2:

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
17天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
28 1
|
2月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
47 1
|
2月前
|
存储 Java
告别混乱!用Java Map优雅管理你的数据结构
【10月更文挑战第17天】在软件开发中,随着项目复杂度增加,数据结构的组织和管理至关重要。Java中的Map接口提供了一种优雅的解决方案,帮助我们高效、清晰地管理数据。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,有效提升了代码质量和维护性。
93 2
|
18天前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
4天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
22 5
|
18天前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
26天前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
87 23
|
26天前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
57 20
|
26天前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
44 0
|
1月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
47 6

热门文章

最新文章