算法第一周(排序初级)

冒泡排序(Bubble Sort)

  public static void bubbleSort(int[] nums) {
    int size = nums.length;
    // 每轮针对前面(size-i)个数进行排序
    for (int i = 0; i < size - 1; i++) {
      System.out.println("第" + (i + 1) + "轮交换开始");
      // 每一轮排序,从第 0 个元素,到 size-1-i 个元素
      for (int j = 0; j < size - 1 - i; j++) {
        // 对比相邻的两个元素
        if (nums[j] > nums[j + 1]) {
          // 元素交换。使得大的元素在后面
          int temp = nums[j + 1];
          nums[j + 1] = nums[j];
          nums[j] = temp;
        }
        printf(nums);
      }
    }
  }

 冒泡排序(Bubble Sort)是基于交换的排序,每次遍历需要排序的元素,依次比较相邻的两个元素的大小,如果前一个元素大于后一个元素则两者交换,保证最后一个数字一定是最大的(假设按照从小到大排序),即最后一个元素已经排好序,下一轮只需要保证前面 n-1 个元素的顺序即可。

由于冒泡排序只会交换相邻的元素,它不会出现两个相等的元素,后面的元素被交换到前面去的情况(相等的时候,我们可以控制两者不交换),所以冒泡排序是稳定的。

上面计算时间复杂度是 O(n2),时间复杂度取系数最高的项即可,当然这是最坏情况下的时间复杂度。

选择排序(SelectionSort)        

public class SelectionSort {
    public static void main(String[] args) {
        int[]nums = new int[]{98,90,34,56,21};
        printf(nums);
        selectionSort(new int[]{98,90,34,56,21});
    }

    private static int[] selectionSort(int[] nums) {
        int times = 0;
        int minIndex ,temp;
        for (int i = 0; i < nums.length - 1; i++) {
            System.out.println("第" + (i+1) +"轮选择开始:");
            minIndex = i;
            for(int j = i+1; j < nums.length  ; j++) {
                times++;
                if(nums[minIndex] > nums[j]) {
                    minIndex = j;
                }
            }
            System.out.println("交换"+nums[i] + "和"+nums[minIndex]);
            temp = nums[i];
            nums[i] = nums[minIndex];
            nums[minIndex] = temp;
        }
        System.out.println("比较次数" +times);
        return nums;
    }

    private static void printf(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            System.out.print(nums[i]+" ");
        }

    }
}

前面说的冒泡排序是每一轮比较确定最后一个元素,中间过程不断地交换。而选择排序就是每次选择剩下的元素中最小的那个元素,与当前索引位置的元素交换,直到所有的索引位置都选择完成。

由于选择排序只会选择最小的元素进行交换,如果我们可以保证我们每次选择到的最小元素是第一次出现的(就算后面出现大小相等的元素我们也不会选择后面的),那么就可以保证它的稳定性,所以选择排序是可以做到稳定的。O(n2)

选择排序和冒泡排序的区别是什么呢?

冒泡排序在比较的时候会不断的交换,直到最大/最小的冒泡到最后的位置,而选择排序则是在剩下的元素里面选择出最小或者最大的,中间一般不会发生交换,只会维护好最小/最大值的索引,与当前需要排序的位置元素交换。

插入排序(InsertSort

插入排序是依次选择一个元素,插入到前面已经排好序的数组中间,确保它处于正确的位置,当然,这是需要已经排好的顺序数组不断移动。步骤描述如下:

  1. 从第一个元素开始,可以认为第一个元素已经排好顺序。
  2. 取出后面一个元素 n,在前面已经排好顺序的数组里从尾部往头部遍历,假设正在遍历的元素为 nums[i],如果 num[i] > n,那么将 nums[i] 移动到后面一个位置,直到找到已经排序的元素小于或者等于新元素的位置,将 n 放到新腾空出来的位置上。如果没有找到,那么 nums[i] 就是最小的元素,放在第一个位置。
  3. 重复上面的步骤 2,直到所有元素都插入到正确的位置。
    public static void insertionSort(int[] nums) {
        if (nums == null) {
          return;
        }
        int size = nums.length;
        int index, temp;
        for (int i = 1; i < size; i++) {
          // 当前选择插入的元素前面一个索引值
          index = i - 1;
          // 当前需要插入的元素
          temp = nums[i];
          while (index >= 0 && nums[index] > temp) {
            nums[index + 1] = nums[index];
            index--;
          }
          // 插入空出来的位置
          nums[index + 1] = temp;
          System.out.print("第" + (i) + "轮插入结果:");
          printf(nums);
        }
      }

    由于插入排序只会选择元素插入到适合的位置,只要我们按照原来的顺序遍历,即使相等的两个元素最后排完顺序之后,也会保持原有的相对顺序,所以插入排序是稳定的。O(n2)

计数排序(CountSort)

由于只做计数,不会大部分交换,根据统计数组来回复排序数组的时候是可以保持元素的相对位置的,所以是稳定排序。

  • 遍历数组,找出最大值和最小值。
  • 根据最大值和最小值,初始化对应的统计元素数量的数组。
  • 遍历元素,统计元素个数到新的数组。
  • 遍历统计的数组,按照顺序输出排序的数组元素。 
       
    public class CountSort {
    
      public static void countSort(int[] nums) {
        int max = nums[0];
        int min = nums[0];
        for (int i = 1; i < nums.length; i++) {
          if (nums[i] > max) {
            max = nums[i];
          }
          if (nums[i] < min) {
            min = nums[i];
          }
        }
        System.out.println("min:" + min + ",max:" + max);
        int count = max - min;
        int[] countNums = new int[count + 1];
        for (int i = 0; i < nums.length; i++) {
          countNums[nums[i] - min]++;
        }
        System.out.print("countNums: ");
        printf(countNums);
        int sum = 0;
        // 后面的元素等于前面元素加上自身
        for (int i = 0; i < count + 1; i++) {
          sum += countNums[i];
          countNums[i] = sum;
        }
        System.out.print("countNums: ");
        printf(countNums);
        int[] newNums = new int[nums.length];
        for (int i = nums.length - 1; i >= 0; i--) {
          /**
           * nums[i] - min 表示原数组 nums 里面第i位置对应的数在统计数组里面的位置索引
           */
          newNums[countNums[nums[i] - min] - 1] = nums[i];
          countNums[nums[i] - min]--;
        }
        printf(newNums);
      }
    }
    

    计数排序,在一定范围数值的数组,假设元素在 0~k 之间,一共 n 个数,那么只需要遍历 n 个数,就可以统计,统计的时候,只需要遍历 k 个数,就可以将排序的元素移动到数组中。时间复杂度为 O(n+k),申请了一个统计数组和一个新数组,空间复杂度为 O(n+k)。

    计数排序有严重的缺点:

  • 如果数列的数组最大值与最小值相差太大,会浪费较大空间。
  • 数组元素不是整数不适合计数排序。

     

基数排序(RadixSort)

基数排序比较特殊,特殊在它只能用在整数(自然数)排序,而且不是基于比较的,其原理是将整数按照位分成不同的数字,按照每个数各位值逐步排序。何为高位,比如 81,1 就是低位, 8 就是高位。 分为高位优先和低位优先,先比较高位就是高位优先,先比较低位就是低位优先。下面我们讲高位优先。

public class RadixSort {

  private static void radixSort(int[] nums) {
    int max = nums[0];
    // 指数,从个位到十位到百位...
    int exp;
    // 遍历得到最大值
    for (int num : nums) {
      if (num > max) {
        max = num;
      }
    }
    // 从个位开始,对数组每一位进行排序
    for (exp = 1; max / exp > 0; exp = exp * 10) {
      // 临时数组
      int[] tempNums = new int[nums.length];
      // 数值 0-9,桶的个数固定为 10
      int[] buckets = new int[10];
      // buckets 中存储的其实是数据出现的次数
      for (int value : nums) {
        buckets[(value / exp) % 10]++;
      }
      // 每一个值等于前面的元素次数加上自身(类似计数排序)
      for (int i = 1; i < 10; i++) {
        buckets[i] += buckets[i - 1];
      }
      // 从后往前遍历,将元素写会临时数组
      for (int i = nums.length - 1; i >= 0; i--) {
        tempNums[buckets[(nums[i] / exp) % 10] - 1] = nums[i];
        buckets[(nums[i] / exp) % 10]--;
      }
      // 将有序元素 tempNums 赋给 nums
      System.arraycopy(tempNums, 0, nums, 0, nums.length);
      printf(nums);
    }
  }
}

一般只使用于整数排序,不适合小数或者文字排序.时间O(n)

希尔排序(ShellSort)❤

希尔排序(Shell's Sort)又称“缩小增量排序”(Diminishing Increment Sort),是插入排序的一种更高效的改进版本,同时该算法是首次冲破 O(𝑛2n2) 的算法之一。

希尔排序也是面试官比较喜欢问的一个话题,它的特点是排序的间隔 gap 不断缩小。

  1. 选择一个增量 gap,一般开始是数组的一半,将数组元素按照间隔为 gap 分为若干个小组。
  2. 对每一个小组进行插入排序。
  3. 将 gap 缩小为一半,重新分组,重复步骤 2(直到 gap 为 1 的时候基本有序,稍微调整一下即可)。
public static void shellSort(int[] nums) {
        int times = 1;
        for (int gap = nums.length / 2; gap > 0; gap /= 2) {
            System.out.print(
                    "第" + (times++) + "轮希尔排序, gap= " + gap + " ,结果:"
            );
            for (int i = gap; i < nums.length; i++) {
                int j = i;
                int temp = nums[j];
                while (j - gap >= 0 && temp < nums[j - gap]) {
                    // 移动法
                    nums[j] = nums[j - gap];
                    j -= gap;
                }
                nums[j] = temp;
            }
            printf(nums);
        }
    }

每次取数组的一半,希尔增量下最坏的情况时间复杂度是 O(𝑛2n2),最好的时间复杂度是 O(𝑛n) (也就是数组已经有序),平均时间复杂度是 O(𝑛3/2n3/2)

由于希尔排序,在分组的时候,会将后面的元素间隔性的调动到前面,所以会打乱原本两个相等的数之间的相对顺序,所以希尔排序是不稳定的排序算法。

快速排序(QuickSort)❤❤

快速排序比较有趣,选择数组的一个数作为基准数,一趟排序,将数组分割成为两部分,一部分均小于/等于基准数,另外一部分大于/等于基准数。然后分别对基准数的左右两部分继续排序,直到数组有序。这体现了分而治之的思想,其中还应用到挖坑填数的策略。

nums 0 nums.length-1
System.out.println("["+left+" , "+right+"]");
        if(left < right) {
            // 区间左边界是i,右边界是j,基准值是 standardNum
            int i = left, j = right,standardNum = nums[left];
            while (i < j) {
                //从右向左找第一个小于 standard
                while (i < j && nums[j] >= standardNum) {
                    j--;
                }
                System.out.println("standardNum = "+standardNum+", 第一个小于等于standardNum = "+nums[i]);
                if (i < j) {
                    //nums[i]已经被保存到standardNum 将nums【i】写到左边
                    nums[i] = nums[j];
                    i++;
                }
                // 从左向右找第一个大于等于standardNum的数
                while (i < j && nums[i] <= standardNum) {
                    i++;
                }
                System.out.println(",第一个大于等于standardNum = "+nums[i]);
                if (i < j) {
                    //将较大的数写到右边
                    nums[j] = nums[i];
                    j--;
                }
            }
            // 将基准值写到中间
            nums[i] = standardNum;
            printf(nums);
            quickSort(nums,left,i-1);
            printf(nums);
            quickSort(nums,i+1,right);

由于快速排序会将一个数大间隔的移动到一边,大的数放在右边,小的数放在左边,所以会破坏两个相等的元素的相对顺序,所以它是不稳定的排序算法。

桶排序(BucketSort)

桶排序,是指用多个桶存储元素,每个桶有一个存储范围,先将元素按照范围放到各个桶中,每个桶中是一个子数组,然后再对每个子数组进行排序,最后合并子数组,成为最终有序的数组。这其实和计数排序很相似,只不过计数排序每个桶只有一个元素,而且桶存储的值为该元素出现的次数。

public static void bucketSort(int[] nums) {
    // 遍历数组获取最大最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (int i = 0; i < nums.length; i++) {
      max = Math.max(max, nums[i]);
      min = Math.min(min, nums[i]);
    }

    // 计算桶的数量
    int bucketNum = (max - min) / nums.length + 1;
    System.out.println(
      "最小:" + min + ",最大:" + max + ",桶的数量:" + bucketNum
    );
    List<List<Integer>> buckets = new ArrayList<List<Integer>>(bucketNum);
    for (int i = 0; i < bucketNum; i++) {
      buckets.add(new ArrayList<Integer>());
    }

    // 将每个元素放入桶
    for (int i = 0; i < nums.length; i++) {
      int num = (nums[i] - min) / (nums.length);
      buckets.get(num).add(nums[i]);
    }

    // 对每个桶内部进行排序
    for (int i = 0; i < buckets.size(); i++) {
      Collections.sort(buckets.get(i));
    }

    // 将桶的元素复制到数组中
    int index = 0;
    for (int i = 0; i < buckets.size(); i++) {
      for (int j = 0; j < buckets.get(i).size(); j++) {
        nums[index++] = buckets.get(i).get(j);
      }
    }
  }

  public static void printf(int[] nums) {
    for (int num : nums) {
      System.out.print(num + " ");
    }
    System.out.println("");
  }

至于排序的稳定性,桶排序的稳定性取决于桶内部的排序,如果桶内使用快速排序则整体桶排序表现为不稳定排序。

堆排序(HeapSort)

堆排序,就是利用大顶堆或者小顶堆来设计的排序算法,是一种选择排序。堆是一种完全二叉树:

  • 大顶堆:每个节点的数值都大于或者等于其左右孩子节点的数值。
  • 小顶堆:每个节点的数值都小于或者等于其左右孩子节点的数值。
public static void heapSort(int[] nums) {
    // 首先需要构建最大堆
    for (int i = nums.length / 2 - 1; i >= 0; i--) {
      // 从第一个非叶子结点调整结构,大的往上走
      adjustHeap(nums, i, nums.length);
    }
    printf(nums);
    System.out.println("-----------------------------");
    // 交换元素和调整
    for (int j = nums.length - 1; j > 0; j--) {
      // 将堆顶元素与末尾元素交换
      swap(nums, 0, j);
      // 重新调整,大的元素往上交换
      adjustHeap(nums, 0, j);
      printf(nums);
      System.out.println("-----------------------------");
    }
  }

  /**
   * 调整大顶堆
   */
  public static void adjustHeap(int[] nums, int i, int length) {
    // 取出当前元素
    int temp = nums[i];
    //从左节点开始
    for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
      // 如果右节点更大,那么指向右节点
      if (k + 1 < length && nums[k] < nums[k + 1]) {
        k++;
      }
      // 子节点的值直接给父节点
      if (nums[k] > temp) {
        nums[i] = nums[k];
        i = k;
      } else {
        break;
      }
      printf(nums);
    }
    // 最后将最上面的节点置,放到当前的节点
    nums[i] = temp;
  }

  /**
   * 交换元素
   */
  public static void swap(int[] nums, int a, int b) {
    int temp = nums[a];
    nums[a] = nums[b];
    nums[b] = temp;
  }

  public static void printf(int[] nums) {
    for (int num : nums) {
      System.out.print(num + " ");
    }
    System.out.println("");
  }
}

由于在调整堆的时候,会修改相等元素的相对位置,所以堆排序是不稳定的排序算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值