演算法思維體操:用JavaScript和Python自己實現reduceRight和map(連結串列)

冒泡的馬樹發表於2023-02-25

引言

我們從一個連結串列的建構函式開始,“cons”,它接收一個必要引數“head”及一個可選引數“tail”(相對於支援這樣的實現的語言來說)。該建構函式返回一個連結串列的表示結構體,其中第一個元素為“head”,其餘的元素被包裹在“tail“連結串列中。空連結串列的話用JavaScript中的undefined或Python中的None來表示。

舉例來說,直到微小變化,”cons(1, cons(2, cons(3, cons(4))))"構造了一個含有4個元素的連結串列,”1 2 3 4“。

為了便於檢查連結串列中的內容,我們定義了”listToString“方法來將連結串列轉換為相應的字串,其中連結串列元素之間用空格隔開。

”myMap“方法接收一個一元函式”fn“和一個連結串列”list“。它循序遍歷連結串列中的每一個元素,並返回一個各元素都被”fn“轉化過了的連結串列。

”myReduce"方法會對輸入的連結串列從頭到尾使用一個reducer函式“fn”,然後返回最終結果。比如,假設連結串列為“cons(1, cons(2, cons(3,)))”,“myReduce(fn, accm, list)”應該返回執行“fn(fn(fn(accm, 1), 2), 3)”得到的結果。

上述的三個方法都是使用遞迴實現的,巧妙運用了連結串列的遞迴結構。

第1部分:實現“myReduceRight"

請實現“myReduceRight”方法。其類似於“myReduce”,不同之處在於它是從尾到頭對輸入的連結串列使用reducer函式“fn”的。比如,假設連結串列為“cons(1, cons(2, cons(3)))",”myReduceRight(fn, accm, list)"應該返回執行“fn(1, fn(2, fn(3, accm)))"得到的結果。

要求:

  1. 需要使用遞迴來實現,而不是任何顯式的for/while迴圈;
  2. 不能在你的實現中使用先前已定義好的”listToString“、”myMap“和”myReduce“方法;
  3. 不能修改原始連結串列。

要檢查你的實現的正確性,可以驗證:

  • myReduceRight(xTimesTwoPlusY, 0, exampleList)“應該得到“20”;
  • myReduceRight(unfoldCalculation, accm, exampleList)"應該表示為”fn(1, fn(2, fn(3, fn(4, accm))))";
  • myReduceRight(printXAndReturnY, 0, exampleList)"應該按照輸入連結串列的逆序來列印內容。

第2部分:實現”myMap2“

請基於“myReduceRight"方法實現”myMap2“,其應該在功能性上同質於”myMap“。

對實現的基本要求:

  1. 不能在你的實現中使用先前已定義好的”listToString“、”myMap“和”myReduce“方法;
  2. 不能修改任何函式的輸入輸出特徵,包括”myReduceRight“的輸入輸出特徵;
  3. 不能在借在”myReduceRight“中投機取巧來助力實現”myMap2“,例如向”myReduceRight“傳遞隱藏標誌以表示特殊處理;
  4. 不能使用任何語言原生的特殊資料結構(舉例,C++中的”std::vector",Java中的“ArrayList”,Python中的“list”)。

如果你的實現滿足以下儘可能多的要求,你將獲得“加分”:

  1. 不要使用任何顯式的遞迴呼叫。特別地,避免在實現中宣告呼叫“myMap2”;
  2. 不要在實現中使用任何顯式的for/while迴圈。為此你需要探究下“myReduceRight”的巧妙用法;
  3. 不要修改原始連結串列。

以下是你可以遵循的幾個方向:

  • 列表翻轉;
  • 在reducer方法中修改連結串列;
  • 巧妙地使用閉包和lambda函式來調整程式碼執行順序。特別地,考慮考慮延時執行,如(() -> doSomething)()。

要檢查你的實現的正確性,可以驗證:

  • listToString(myMap2(plusOne, exampleList))”應該得到“2 3 4 5”;
  • myMap2(printAndReturn, exampleList)”應該按照正確的次序列印連結串列內容(“1 2 3 4”分別各佔據一行而不是“4 3 2 1”)。

JavaScript程式碼模板:

// Refer to README for detailed instructions.

function cons(head, tail) {
  return {
    head: head,
    tail: tail,
  };
}

function listToString(list) {
  if (!list) {
    return '';
  }
  if (!list.tail) {
    return list.head.toString();
  }
  return list.head.toString() + ' ' + listToString(list.tail);
}

function myMap(fn, list) {
  if (!list) {
    return undefined;
  }
  return cons(fn(list.head), myMap(fn, list.tail));
}

function myReduce(fn, accm, list) {
  if (!list) {
    return accm;
  }
  return myReduce(fn, fn(accm, list.head), list.tail);
}

function myReduceRight(fn, accm, list) {
  // [BEGIN] YOUR CODE HERE
  return undefined;
  // [END] YOUR CODE HERE
}

function myMap2(fn, list) {
  // [BEGIN] YOUR CODE HERE
  return undefined;
  // [END] YOUR CODE HERE
}

function main() {
  let exampleList = cons(1, cons(2, cons(3, cons(4))));
  let plusOne = (x) => x + 1;
  let xTimesTwoPlusY = (x, y) => x * 2 + y;
  let printXAndReturnY = (x, y) => {
    console.log(x);
    return y;
  };
  let unfoldCalculation = (x, y) => 'fn(' + x + ', ' + y + ')';
  let printAndReturn = console.log;
  console.log(listToString(exampleList), 'should be 1 2 3 4');
  console.log(listToString(myMap(plusOne, exampleList)), 'should be 2 3 4 5');
  console.log(myReduce(xTimesTwoPlusY, 0, exampleList), 'should be 26');
  console.log(
    myReduce(unfoldCalculation, 'accm', exampleList),
    'should be fn(fn(fn(fn(accm, 1), 2), 3), 4)'
  );
  console.log(myReduceRight(xTimesTwoPlusY, 0, exampleList), 'should be 20');
  console.log(
    myReduceRight(unfoldCalculation, 'accm', exampleList),
    'should be fn(1, fn(2, fn(3, fn(4, accm))))'
  );
  console.log('Below should output 4 3 2 1 each on a separate line:');
  myReduceRight(printXAndReturnY, 0, exampleList);
  console.log(listToString(myMap2(plusOne, exampleList)), 'should be 2 3 4 5');
  console.log('The two outputs below should be equal:');
  console.log('First output:');
  myMap(printAndReturn, exampleList);
  console.log('Second output:');
  myMap2(printAndReturn, exampleList);
}

main();

Python程式碼模板:

# Refer to README for detailed instructions.

from __future__ import print_function


class LinkedList:
    def __init__(self, head, tail):
        self.head = head
        self.tail = tail


def cons(head, tail=None):
    return LinkedList(head, tail)


def listToString(list):
    if list is None:
        return ""
    if list.tail is None:
        return str(list.head)
    return str(list.head) + " " + listToString(list.tail)


def myMap(fn, list):
    if list is None:
        return None
    return cons(fn(list.head), myMap(fn, list.tail))


def myReduce(fn, accm, list):
    if list is None:
        return accm
    return myReduce(fn, fn(accm, list.head), list.tail)


def myReduceRight(fn, accm, list):
    # [BEGIN] YOUR CODE HERE
    return None
    # [END] YOUR CODE HERE


def myMap2(fn, list):
    # [BEGIN] YOUR CODE HERE
    return None
    # [END] YOUR CODE HERE


def main():
    exampleList = cons(1, cons(2, cons(3, cons(4))))
    plusOne = lambda x: x + 1
    xTimesTwoPlusY = lambda x, y: x * 2 + y
    def printXAndReturnY(x, y):
        print(x)
        return y
    def unfoldCalculation(x, y):
        return "fn(%s, %s)" % (str(x), str(y))
    printAndReturn = print
    print(listToString(exampleList), "should be 1 2 3 4")
    print(listToString(myMap(plusOne, exampleList)), "should be 2 3 4 5")
    print(myReduce(xTimesTwoPlusY, 0, exampleList), "should be 26")
    print(myReduce(unfoldCalculation, "accm", exampleList), "should be fn(fn(fn(fn(accm, 1), 2), 3), 4)")
    print(myReduceRight(xTimesTwoPlusY, 0, exampleList), "should be 20")
    print(myReduceRight(unfoldCalculation, "accm", exampleList), "should be fn(1, fn(2, fn(3, fn(4, accm))))")
    print("Below should output 4 3 2 1 each on a separate line:");
    myReduceRight(printXAndReturnY, 0, exampleList)
    print(listToString(myMap2(plusOne, exampleList)), "should be 2 3 4 5")
    print("The two outputs below should be equal:")
    print("First output:")
    myMap(printAndReturn, exampleList)
    print("Second output:")
    myMap2(printAndReturn, exampleList)


if __name__ == "__main__":
    main()

最終實現:

JavaScript實現:

// Refer to README for detailed instructions.

function cons(head, tail) {
  return {
    head: head,
    tail: tail,
  };
}

function listToString(list) {
  if (!list) {
    return '';
  }
  if (!list.tail) {
    return list.head.toString();
  }
  return list.head.toString() + ' ' + listToString(list.tail);
}

function myMap(fn, list) {
  if (!list) {
    return undefined;
  }
  return cons(fn(list.head), myMap(fn, list.tail));
}

function myReduce(fn, accm, list) {
  if (!list) {
    return accm;
  }
  return myReduce(fn, fn(accm, list.head), list.tail);
}

function myReduceRight(fn, accm, list) {
  // [BEGIN] YOUR CODE HERE
  if (!list) {
    return accm;
  }

  // State-of-the-art trampoline trick to prevent recursion stack overflow
  const trampoline = (fun) => {
    return function trampolined(...args) {
      var result = fun(...args);

      while (typeof result == 'function') {
        result = result();
      }

      return result;
    };
  };

  const reverseOperation = (origList) => {
    const reverseCons = (cons, acc = []) => {
      if (!cons) {
        return undefined;
      }
      acc.push(cons.head);
      if (cons.tail instanceof Object) {
        return reverseCons(cons.tail, acc);
      } else {
        return acc.reverse();
      }
    };

    const recursCons = (jsList = []) => {
      if (jsList.length <= 0) {
        return undefined;
      } else {
        return {
          head: jsList[0],
          tail: recursCons(jsList.slice(1)),
        };
      }
    };

    // IMMUTABLE
    const newList = Object.assign({}, origList);
    //   Get the reversed version of Linklist in another plain representation
    const reversedJSList = trampoline(reverseCons)(newList, []);
    // Back assign the reversed plain representation to Linklist
    const reversedLinkList = trampoline(recursCons)(reversedJSList);

    return reversedLinkList;
  };

  const innerReducer = (fn_, accm_, list_) => {
    if (!list_) {
      return accm_;
    }
    return innerReducer(fn_, fn_(list_.head, accm_), list_.tail);
  };

  return trampoline(innerReducer)(fn, accm, reverseOperation(list));
  // [END] YOUR CODE HERE
}

function myMap2(fn, list) {
  // [BEGIN] YOUR CODE HERE

  // State-of-the-art trampoline trick to prevent recursion stack overflow
  const trampoline = (fun) => {
    return function trampolined(...args) {
      var result = fun(...args);

      while (typeof result == 'function') {
        result = result();
      }

      return result;
    };
  };

  const polishedFn = (cur, acc) => {
    let newAcc = {};
    newAcc.tail = Object.keys(acc).length > 0 ? acc : undefined;
    newAcc.head = () => fn(cur); // delay to keep the map order
    return newAcc;
  };

  let newList = Object.assign(list);

  const storeList = myReduceRight(polishedFn, {}, newList);

  const activateStore = (store) => {
    if (!store) return undefined;
    store.head = store.head instanceof Function ? store.head() : store.head;
    store.tail = activateStore(store.tail);
    return store;
  };

  return trampoline(activateStore)(storeList);
  // [END] YOUR CODE HERE
}

function main() {
  let exampleList = cons(1, cons(2, cons(3, cons(4))));
  let plusOne = (x) => x + 1;
  let xTimesTwoPlusY = (x, y) => x * 2 + y;
  let printXAndReturnY = (x, y) => {
    console.log(x);
    return y;
  };
  let unfoldCalculation = (x, y) => 'fn(' + x + ', ' + y + ')';
  let printAndReturn = console.log;
  console.log(listToString(exampleList), 'should be 1 2 3 4');
  console.log(listToString(myMap(plusOne, exampleList)), 'should be 2 3 4 5');
  console.log(myReduce(xTimesTwoPlusY, 0, exampleList), 'should be 26');
  console.log(
    myReduce(unfoldCalculation, 'accm', exampleList),
    'should be fn(fn(fn(fn(accm, 1), 2), 3), 4)'
  );
  console.log(myReduceRight(xTimesTwoPlusY, 0, exampleList), 'should be 20');
  console.log(
    myReduceRight(unfoldCalculation, 'accm', exampleList),
    'should be fn(1, fn(2, fn(3, fn(4, accm))))'
  );
  console.log('Below should output 4 3 2 1 each on a separate line:');
  myReduceRight(printXAndReturnY, 0, exampleList);
  console.log(listToString(myMap2(plusOne, exampleList)), 'should be 2 3 4 5');
  console.log('The two outputs below should be equal:');
  console.log('First output:');
  myMap(printAndReturn, exampleList);
  console.log('Second output:');
  myMap2(printAndReturn, exampleList);
}

main();
訣竅:使用蹦床函式trampoline最佳化遞迴呼叫。

列印結果:

'1 2 3 4' 'should be 1 2 3 4'
'2 3 4 5' 'should be 2 3 4 5'
26 'should be 26'
'fn(fn(fn(fn(accm, 1), 2), 3), 4)' 'should be fn(fn(fn(fn(accm, 1), 2), 3), 4)'
20 'should be 20'
'fn(1, fn(2, fn(3, fn(4, accm))))' 'should be fn(1, fn(2, fn(3, fn(4, accm))))'
'Below should output 4 3 2 1 each on a separate line:'
4
3
2
1
'2 3 4 5' 'should be 2 3 4 5'
'The two outputs below should be equal:'
'First output:'
1
2
3
4
'Second output:'
1
2
3
4

Python實現:

# Refer to README for detailed instructions.

from __future__ import print_function
import copy
import types

class LinkedList:
    def __init__(self, head, tail):
        self.head = head
        self.tail = tail


def cons(head, tail=None):
    return LinkedList(head, tail)


def listToString(list):
    if list is None:
        return ""
    if list.tail is None:
        return str(list.head)
    return str(list.head) + " " + listToString(list.tail)


def myMap(fn, list):
    if list is None:
        return None
    return cons(fn(list.head), myMap(fn, list.tail))


def myReduce(fn, accm, list):
    if list is None:
        return accm
    return myReduce(fn, fn(accm, list.head), list.tail)


def myReduceRight(fn, accm, list):
    # [BEGIN] YOUR CODE HERE
    if list is None:
        return accm

    def reverseOperation(origList):
        # Get the reversed version of Linklist in another plain representation
        def reverseCons(cons, acc = []):
            if cons is None:
                return None
            acc.append(cons.head)
            if cons.tail != None:
                return reverseCons(cons.tail, acc)
            else:
                return acc[::-1]
        # Back assign the reversed plain representation to Linklist
        def recursCons(pyList = []):
            if len(pyList) <= 0:
                return None
            else:
                return cons(pyList[0], recursCons(pyList[1:]))

        newList = copy.deepcopy(origList)
        reversedPyList = reverseCons(newList, []);
        reversedLinkList = recursCons(reversedPyList);

        return reversedLinkList

    def innerReducer(fn_, accm_, list_):
        if list_ is None:
            return accm_
        return innerReducer(fn_, fn_(list_.head, accm_), list_.tail)

    return innerReducer(fn, accm, reverseOperation(list));
    # [END] YOUR CODE HERE

def myMap2(fn, list):
    # [BEGIN] YOUR CODE HERE
    def polishedFn(cur, acc):
        newAcc = cons(None)
        newAcc.tail = acc if isinstance(acc, LinkedList) else None
        newAcc.head = lambda: fn(cur) # delay to keep the map order
        return newAcc

    newList = copy.deepcopy(list)

    storeList = myReduceRight(polishedFn, {}, newList);

    def activateStore(store):
        if store is None:
            return None
        store.head = store.head() if isinstance(store.head, types.FunctionType) else store.head
        store.tail = activateStore(store.tail)
        return store

    return activateStore(storeList)
    # [END] YOUR CODE HERE


def main():
    exampleList = cons(1, cons(2, cons(3, cons(4))))
    plusOne = lambda x: x + 1
    xTimesTwoPlusY = lambda x, y: x * 2 + y
    def printXAndReturnY(x, y):
        print(x)
        return y
    def unfoldCalculation(x, y):
        return "fn(%s, %s)" % (str(x), str(y))
    printAndReturn = print
    print(listToString(exampleList), "should be 1 2 3 4")
    print(listToString(myMap(plusOne, exampleList)), "should be 2 3 4 5")
    print(myReduce(xTimesTwoPlusY, 0, exampleList), "should be 26")
    print(myReduce(unfoldCalculation, "accm", exampleList), "should be fn(fn(fn(fn(accm, 1), 2), 3), 4)")
    print(myReduceRight(xTimesTwoPlusY, 0, exampleList), "should be 20")
    print(myReduceRight(unfoldCalculation, "accm", exampleList), "should be fn(1, fn(2, fn(3, fn(4, accm))))")
    print("Below should output 4 3 2 1 each on a separate line:");
    myReduceRight(printXAndReturnY, 0, exampleList)
    print(listToString(myMap2(plusOne, exampleList)), "should be 2 3 4 5")
    print("The two outputs below should be equal:")
    print("First output:")
    myMap(printAndReturn, exampleList)
    print("Second output:")
    myMap2(printAndReturn, exampleList)


if __name__ == "__main__":
    main()
備註:Python的trampoline蹦床函式實現有些複雜,直接遞迴處理了。可參考文章Tail recursion in Python, part 1: trampolines)。

列印結果:

1 2 3 4 should be 1 2 3 4
2 3 4 5 should be 2 3 4 5
26 should be 26
fn(fn(fn(fn(accm, 1), 2), 3), 4) should be fn(fn(fn(fn(accm, 1), 2), 3), 4)
20 should be 20
fn(1, fn(2, fn(3, fn(4, accm)))) should be fn(1, fn(2, fn(3, fn(4, accm))))
Below should output 4 3 2 1 each on a separate line:
4
3
2
1
2 3 4 5 should be 2 3 4 5
The two outputs below should be equal:
First output:
1
2
3
4
Second output:
1
2
3
4

相關文章