Clojure:算法练习的实现(一)——合并排序

时间:2022-03-05 22:49:20

*本系列文章使用Clojure来实现一些经典的算法,主要目的用于记录笔者对算法的重新学习(因此可能不会有过多的说明)Clojure:算法练习的实现(一)——合并排序

合并排序(Merge Sort)

产生随机数组

首先我们先给出一个产生随机数组的函数,这个函数随机地产生n个从1到m的整数数组:

(defn rnm [n m]
  "Generates a n-size int array. The number ranges from 1 to m [1, m]."
  (vec (repeatedly n #(+ 1 (rand-int m)))))

Merge函数

然后我们实现merge函数。这里我们给出merge算法的伪代码描述。为了简化,我们假设将大小相同的数组a1、a2合并,并假设合并的数组大小为m(意味着a1和a2的大小各为m/2):

==============================

i = 1; j = 1;

for k = 1 to m

  if a1[i] <= a2[j]

    r[k] = a1[i];

    i++;

  else

    r[k] = a2[j];

    j++;

  end

end

return r;

==============================

用上述算法我们的merge函数如下(为了区别Clojure已有的merge函数我们的merge命名为merge1):

(defn merge1 [a1 a2]
  "Merges elements from array1 and array2 in ascend order."
  (loop [i 0 j 0 result []]
    (if (or (= i (count a1)) (= j (count a2)))
      (if (= i (count a1)) 
        (apply conj result (subvec a2 j))
        (apply conj result (subvec a1 i)))
      (if (<= (a1 i) (a2 j))
        (recur (+ i 1) j (conj result (a1 i)))
        (recur i (+ j 1) (conj result (a2 j)))))))

要注意的是我们的代码处理了a1和a2具有不同长度的情况,当任意一个数组被遍历完毕,另外一个数组剩下的元素将被直接拼接到结果数组的后面去。

另外要注意的是merge函数只对已排序的数组有效,因此我们在测试以上代码的时候必须给它提供已排序好的数组,如:

user=> (def a1 (vec (sort (rnm 10 100))))
#'user/a1
user=> (def a2 (vec (sort (rnm 15 100))))
#'user/a2
user=> a1 a2
[6 26 27 52 64 66 70 71 81 88]
[6 21 25 28 41 54 54 57 66 76 78 79 83 84 93]
user=> (merge1 a1 a2)
[6 6 21 25 26 27 28 41 52 54 54 57 64 66 66 70 71 76 78 79 81 83 84 88 93]
user=>

小排序函数

我们知道合并排序采用的是分而治之(Divide & Conquer)的策略,大问题被分解成小问题直到问题小到可以被很简单地解决为止,我们的排序到了问题最简单的形式就是数组只包含1~2个元素,对这样规模的数组排序比较简单:

(defn small-sort [small-a]
  (if (= 2 (count small-a))
    (let [[a b] small-a]
      (if (> a b)
        [b a]
        small-a))
    small-a))

如果数组含有两个元素并且第一个元素大于第二个元素的话就交换位置后返回,否则照原样返回。这个排序函数会用在我们的分割函数中。

分割函数

分割函数算是总的函数,它对输入进行分割,并调用合并函数和小排序函数来对整个“大”问题进行合并处理并最终得到结果:

(defn sort1 [a]
  (let [size (count a)]
    (if (<= size 2)
      (small-sort a)
      (let [mid (int (/ size 2))
            a1 (subvec a 0 mid)
            a2 (subvec a mid)]
        (merge1 (sort1 a1) (sort1 a2))))))

至此所有需要的函数都完成了,我们可以运行一下:

user=> (sort1 (rnm 20 100))
[6 16 19 22 23 24 30 30 46 57 65 67 71 80 80 84 85 88 88 93]
user=> (sort1 (rnm 47 10000))
[387 486 673 776 807 985 1068 1624 1793 1927 1979 2573 2592 2796 2865 2919 3313 3401 3808 3932 4045
4756 4925 5194 5258 5328 5438 5456 5758 5852 6004 6017 6081 6483 6494 6499 6698 7340 7465 7706 7923
8007 8247 8618 9073 9356 9980]
user=>

总结

个人感觉函数式编程的代码没有命令式编程的代码直观(也许只是习惯了的问题)和易于分析。光看上面的代码,笔者都需要先析出对应的伪代码才能更方便地进行算法分析。也许以后熟悉之后能找到更直接的方法吧。

以上代码是笔者重温算法并出于个人兴趣,使用了Clojure来实现的合并排序,经过简单测试貌似可以正常使用。如果各位有更好的实现,希望不惜赐教。


------------------------于2012-06-06 编辑更新 ----------------------------

当重新审视上面的算法,发现函数small-sort是多余的,因为merge函数本身已经包含了对元素数量等于2的数组的排序,因此我们可以去掉small-sort函数,并将sort函数改成如下就行了:

(defn sort1 [a]
  (let [size (count a)]
    (if (< size 2)
      a ;--- 修改部分 (small-sort a)
      (let [mid (int (/ size 2))
            a1 (subvec a 0 mid)
            a2 (subvec a mid)]
        (merge1 (sort1 a1) (sort1 a2))))))