数组排序算法:冒泡排序、快速排序、归并排序、堆排序

396 阅读4分钟

1.冒泡排序,O(N^2)

每一趟遍历,将一个最大的数移到序列末尾。需要n趟。

public static void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {//表示需要找arr.length个数
            for (int j = 0; j < arr.length - i - 1; j++) {//表示一轮比较
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                }
            }
        }
    }

2. 快速排序,最好O(NlogN),最坏O(N^2)

  • 时间复杂度:

    • 每次都能等分数组时最好O(NlogN),一共logN层,每层复杂度是O(N)
    • 每次都分为1+N-1时最坏O(N^2),一共N层,每层复杂度O(N)
  • 空间复杂度

    • 最好O(logN),一共logN层递归
    • 最坏O(N),一共N层递归
  • 注意,要区分start、end 和 left、right的区别。

public static void quickSort(int[] arr, int startIndex, int endIndex) {
        if (startIndex > endIndex) {//当只有一个数字的时候
            return;
        }
        //随机选一个数当作pivot,把他放在最前面,这样会更接近最好情况
        // int rnd = new Random().nextInt(endIndex - startIndex + 1) + startIndex;
        // swap(arr, startIndex, rnd);
        int pivot = startIndex;
        int left = startIndex;
        int right = endIndex;
        while (left != right) {//当left和right没有相遇
        
            while (arr[right] > arr[pivot]) {
                right--;
            }
            //最后一次swap后,right往左走了一步,left=right,
            //left就不能再往右走了,所以有left<right条件
            while (left < right && arr[left] <= arr[pivot]) {//别写成nums[left] < nums[pivot]
                left++;
            }
            //到这里,right遇到了比pivot小的数,left遇到了比pivot大的数,所以两者可以交换
            swap(arr, left, right);
        }
        //left和right相遇,这时交换pivot和left即可保证pivot左边小,右边大
        swap(arr, pivot, left);
        // if (left - 1 >= startIndex)
        quickSort(arr, startIndex , left - 1);//递归,对左半部分排序
        // if (endIndex >= left + 1)
        quickSort(arr, left + 1, endIndex);//递归,对右半部分排序
}
    
//交换数组中的两个数
public static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

典型错误:

image.png 关键点:为什么右指针先走? 因为pivot选的是子数组的最左边的元素。在某个子数组的遍历中,left和right的最后一次交换后,left位置上的数字要小于pivot,right位置上的数字要大于pivot,只有right先--,才能保证left和right相遇在比pivot小的值的对应索引处。

3.归并排序,O(NlogN)

  • 时间复杂度:
    • 拆分:一次拆分需要O(1), 一共logN层,所以是O(logN)
    • 合并:一次两两合并的复杂度是O(N),一共logN层,所以是O(NlogN)
    • 总时间复杂度是O(NlogN) 思路:
  • 假设两段子数组[L,M] [M+1,R]已经排好序,需要将两段merge起来。
  • merge的过程:把左半段和右半段分别拷贝出来,然后重新排放到原数组中。
  • merge前mergerSort()递归的过程:递归到子数组长度为1

//归并排序
  public static void mergeSort(int[] arr, int l, int r) {//l为最左边元素的索引,r为最右边元素的索引
      if (l == r) {//当数组长度为1,停止递归
          return;
      } else {
          int m = (l + r) / 2;
          mergeSort(arr, l, m);
          mergeSort(arr, m + 1, r);
          //到这里时,[l,m] [m+1,r], m=(l+r)/2是两段排好序的数组
          merge(arr, l, r);
      }
  }

  //把一个两段排好序的数组搞成从头到尾排序的,两段分别为[l,m] [m+1,r],m=(l+r)/2
  public static void merge(int[] arr, int l, int r) {
      int m = (l + r) / 2;//这里要和上面对应
      int left[] = new int[m - l + 1];//这里要和上面对应
      int right[] = new int[r - m];//这里要和上面对应
      //拷贝左半边
      for (int i = l; i <= m; i++) {//int i = 0 i < m - l + 1; i++ left[i] = arr[i];错!!!
          left[i - l] = arr[i];
      }
      //拷贝右半边
      for (int i = m + 1; i <= r; i++) {//int i = 0 i < r - m; i++ right[i] = arr[i];错!!!
          right[i - m - 1] = arr[i];
      }
      int i = 0, j = 0;
      int k = l;//注意这里k不是0!
      //比较大小后按顺序放入arr
      while (i < left.length && j < right.length) {
          if (left[i] < right[j]) {
              arr[k] = left[i];
              i++;
              k++;
          } else {// if (right[j] < left[i]) 这样写i可能会越界
              arr[k] = right[j];
              j++;
              k++;
          }
      }
      //如果其中一条子数组先走完了,那么直接把另一条剩下的部分放到arr中
      while (i < left.length) {
          arr[k] = left[i];
          i++;
          k++;
      }
      while (j < right.length) {
          arr[k] = right[j];
          j++;
          k++;
      }
  }
  • 典型错误

image.png

4.堆排序, Ο(NlogN)

  • 时间复杂度Ο(NlogN)
  • 空间复杂度O(1)

思路

  • 大顶堆的根节点肯定是堆中的最大元素,每次取之,放到数组最后一位即可
  • 第一步,把数组转化为大顶堆,自底向上heapify,保证最大数能浮上来。
  • 第二步,循环执行:
    • 1.拿出根,放到最后
    • 2.heapify根,注意heapify的范围 bulidHeap过程: heapSort过程:
  public static void heapSort(int[] tree) {
          buildMaxHeap(tree);//上浮,首先把数组转化成一个大顶堆,这样根节点即为数组中元素最大值
          for (int i = tree.length - 1; i >= 0; i--) {
              swap(tree, i, 0);//把根节点(数组现存元素的最大值)和最后一个元素交换
              heapify(tree, i, 0);//下沉,heapify保证大顶,同时把换上来的放到正确的位置。heapify递归范围是根节点到换下来的元素的前一位。
          }
      }
  //把节点i和它后面的节点都给heapify,到索引为n的节点为止 (不包含n)
  //heapify指的是 把一个节点值保证比他的两个孩子大
  public static void heapify(int[] tree, int n, int i) {
      int c1 = 2 * i + 1;//左子节点的索引
      int c2 = 2 * i + 2;//右子节点的索引
      int max = i;
      if (c1 < n && tree[c1] > tree[max]) {//不越界,并且左子节点更大
          max = c1;
      }
      if (c2 < n && tree[c2] > tree[max]) {//不越界,并且右子节点更大
          max = c2;
      }
      if (max != i) {//max == i 表示子节点都比父节点要小
          swap(tree, max, i);
          heapify(tree, n, max);//对大的那个子节点进行heapify,因为换下来的值可能会影响一侧子节点,另一侧还是个heap
      }
  }
  //把一个数组给变成大顶堆
  public static void buildMaxHeap(int[] tree) {
      int lastNode = tree.length - 1;//最后一个节点的索引
      int lastParent = (lastNode - 1) / 2;//最后一个非叶子节点的索引
      //从最下面一层非叶子节点开始,往上层heapify,这样到根节点就是一个heap
      //如果从上往下,可能会出现最大数在最底下冒不上来的情况
      for (int i = lastParent; i >= 0; i--) {
          heapify(tree, tree.length, i);
      }
  }
  • 典型错误

image.png 复杂度分析

时间复杂度为:buildMaxHeap的时间复杂度 + 循环重建堆的时间复杂度

引用

www.zhihu.com/collection/…