排序算法
最经典的、最常用的排序算法:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。
排序算法的三种要素:
- 排序算法的执行效率
最好、最坏、平均情况时间复杂度;
时间复杂度的系数、常数、低阶;
比较次数和交换(移动)次数; - 排序算法的内存消耗
原地排序就是空间复杂度是O(1)的排序算法 - 排序算法的稳定性
待排序列的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
1.冒泡排序
public void bubbleSort(int[] a,int n){
//n表示数据的size
if(n <= 1)
return ;
for(int i = 0;i<n;i++){
boolean flag = false;
for(int j =0;j<n-i-1 ;j++){
if(a[j]>a[j+1]){
int temp = a[j];
a[j]=a[j+1];
a[j+1]=temp;
flag =true;
}
}
if(!flag)
break;
}
}
2.插入排序
public void insertionSort(int[] a,int n){
if (n<=1)
return;
for(int i = 1;i<n;i++){
int temp = a[i];
int j = i-1;
for(;j>=0;j--){
if(a[j]>temp)
a[j+1]=a[j];
else{
break;
}
}
a[j+1]= temp;
}
}
3.选择排序
public void selectionSort(int[] a,int n){
if(n<=1)
return;
for(int i = 0;i<n;i++){
int min = i;
for(int j = i+1;j<n;j++){
if(a[j]<a[min])
min = j;
}
int temp = a[min];
a[min]= a[i];
a[i]= temp;
}
}
如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?
一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。
4. 归并排序
int [] aux;
public void merge_sort(int[]a,int n){
aux = new int[n];
merge_sort_c(a,0,n-1);
}
public void merge_sort_c(int[]a,int lo,int hi){
if(lo>=hi)
return;
int mid = (lo+hi)/2;
merge_sort_c(a,lo,mid);
merge_sort_c(a,mid+1,hi);
merge(a,lo,mid,hi);
}
public void merge(int[]a,int lo,int mid,int hi){
int i = lo,j = mid+1;
for(int k = lo;k<=hi;k++){
aux[k]=a[k];
}
for(int k = lo; k<=hi; k++){
if(lo>mid)
a[k]=aux[j++];
else if(j>hi)
a[k]=aux[i++];
else if(aux[i]<aux[j])
a[k]=aux[i++];
else
a[k]=aux[j++];
}
}
5. 快速排序
public void quick_sort(int[] a ,int n){
quick_sort_c(a,0,n-1);
}
public void quick_sort_c(int[]a,int lo,int hi){
if(lo>=hi)
return;
int mid = partition(a,lo,hi);
quick_sort_c(a,lo,mid-1);
quick_sort_c(a,mid+1,hi);
}
public int partition(int[]a,int lo,int hi){
int i = lo,j = hi+1;
int v = a[lo];
while (true){
if(i<hi&&a[++i]<v){
if(i==hi){
break;
}
}
if(j>lo&&v<a[--j]){
if(j==lo){
break;
}
}
if(i>=j)//-----重点标记
break;
exch(a,i,j);
}
exch(a,lo,j);//将最左值与j所在位置的值进行交换
return j;
}
public void exch(int[] a,int i,int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
如何优化快速排序?
选择一个好的分区点,如果分区点选择的不好,则会造成快速排序退换成复杂度为O(n2)
1.三数取中法;
2.随机法;
归并和快排的时间复杂度是O(nlogn)
归并排序的处理过程是由下而上的,先处理子问题,在合并。而快排相反,处理过程是由上而下的,先分区,然后在处理子问题。
递归虽然是稳定的,但是它是非原地排序算法,合并函数无法在原地执行,而快速排序却可以原地分区,实现原地排序
6.桶排序
核心思想:就是将要排序的数据分到几个有序的桶里,每个桶里的数据在单独进行排序。桶内排完序后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
桶排序对要排数据的要求是非常苛刻的。
首先要排序的数据需要很容易分到m个桶中去,且桶之间有着天然的大小顺序。
其次,数据在各个桶之间的分布是比较均匀的。针对桶内的数据量较大,我们可以继续划分
7.计数排序
计数排序是桶排序的特例。当要排序的n个数据,所处的范围并不大,比如最大为k,我们就可以把数据划分成k个桶,每个桶内的数据值都是相同的,省掉了桶内排序的时间。
计数排序的计数如何而来???
计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了,而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下转化为非负整数。
8. 基数排序
基数排序对要排序的数据是有要求的,要求可以分割出独立的“位”来比较,而且位之间有递进关系,如果a数据的高位比b数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到O(n)了。
9. 排序相关题目
9.1 合并两个有序数组
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
思路:利用归并排序中的merge方法来求解
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[]temp = new int[m];
for(int i =0;i<m;i++){
temp[i]=nums1[i];
}
int i = 0,j=0;
for(int k = 0;k<m+n;k++){
if(i>=m){
nums1[k]=nums2[j++];
}else if(j>=n){
nums1[k]=temp[i++];
}else if(temp[i]<nums2[j]){
nums1[k]=temp[i++];
}else{
nums1[k]=nums2[j++];
}
}
}
9.2 最接近的三数之和
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
思路:直接进行三层循环可以暴力求解,但是太过时间复杂度过高,所以可以选择进行优化。用两个指针进行
public int threeSumClosest(int[] nums, int target) {
//思路一:三层循环,会越界
//思路二:固定一层,其他两层优化
Arrays.sort(nums);
int best = 1000000;
for(int i =0;i<nums.length;i++){
int j = i+1,k=nums.length-1;
while(j<k){
int sum = nums[i]+nums[j]+nums[k];
if(sum == target){
return sum;
}
if(Math.abs(best-target)>Math.abs(sum-target)){
best = sum;
}
if(sum<target){
int j0=j;
while(j0<k&&nums[j0]==nums[j]){
j0++;
}
j=j0;
}else{
int k0=k;
while(k0>j&&nums[k0]==nums[k]){
k0--;
}
k=k0;
}
}
}
return best;
}
此题也可以扩展为求四数之和,只不过在此基础上,在多一层循环,需要注意优化,相同值的情况需要去除掉,双指针
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
ArrayList<List<Integer>> list = new ArrayList<List<Integer>>();
if(nums==null || nums.length<4){
return list;
}
Arrays.sort(nums);
for(int i = 0;i<nums.length-3;i++){
if(i>0 && nums[i]==nums[i-1]){
continue;
}
if(nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target){
break;
}
for(int j =i+1;j<nums.length-2;j++){
if(j>(i+1)&&nums[j]==nums[j-1]){
continue;
}
if(nums[i]+nums[j]+nums[j+1]+nums[j+2]>target){
break;
}
int left = j+1,right=nums.length-1;
while(left<right){
int sum = nums[i]+nums[j]+nums[left]+nums[right];
if(sum==target){
list.add(Arrays.asList(nums[i],nums[j],nums[left],nums[right]));
while(left<right && nums[left]==nums[left+1]){
left++;
}
left++;
while(left<right && nums[right]==nums[right-1]){
right--;
}
right--;
}else if(sum <target){
left++;
}else{
right--;
}
}
}
}
return list;
}
}
9.3 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母都恰好只用一次。
示例 1:
输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]
思路:用hashmap,将异位词排序后作为key,异位词作为value,进行存储,主要的就是利用一些java高级语法。
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List<String>> map = new HashMap<String,List<String>>();
for(String str : strs){
char[] chararray = str.toCharArray();
Arrays.sort(chararray);
String key = new String(chararray);
List<String> list = map.getOrDefault(key,new ArrayList<String>());
list.add(str);
map.put(key,list);
}
return new ArrayList<List<String>>(map.values());
}
9.4数组中的第k个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
思路:利用快排的partition来进行判断,需要注意的是边界条件
public int findKthLargest(int[] nums, int k) {
if(nums==null) {
return -1;
}
int mid = partitions(nums,0,nums.length-1);
while(mid != k-1){
if (mid>k-1){
mid = partitions(nums,0,mid-1);
}else{
mid = partitions(nums,mid+1,nums.length-1);
}
}
return nums[mid];
}
public int partitions(int[]nums,int lo,int hi){
int v = nums[lo];
int i =lo,j=hi+1;
while(true){
while(i<hi&&nums[++i]>v){//开始的条件如果大于hi了也会报错
if(i==hi){
break;
}
}
while (j>lo&&nums[--j]<=v){//需要小于等于号
if(j==lo){
break;
}
}
if(i>=j){
break;
}
exch(nums,i,j);
}
exch(nums,lo,j);
return j;
}
public void exch(int[] a,int i,int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}