开发者

Average of two strings in alphabetical/lexicographical order

Suppose you take the strings 'a' and 'z' and list all the strings that come between them in alphabetical order: ['a','b','c' ... 'x','y','z']. Take the midpoint of this list and you find 'm'. So this is kind of like taking an average of those two strings.

You could extend it to strings with more than one character, for example the midpoint between 'aa' and 'zz' would be found in the middle of the list ['aa', 'ab', 'ac' ... 'zx', 'zy', 'zz'].

Might there be a Python method somewhere that does this? If not, even knowing the name of the algorithm would help.

I began making my own routine that simply goes through both strings and finds midpoint of the first differing letter, which seemed to work great in that 'aa' and 'az' midpoint was 'am', but then it fails on 'cat', 'doggie' midpoint wh开发者_如何转开发ich it thinks is 'c'. I tried Googling for "binary search string midpoint" etc. but without knowing the name of what I am trying to do here I had little luck.

I added my own solution as an answer


If you define an alphabet of characters, you can just convert to base 10, do an average, and convert back to base-N where N is the size of the alphabet.

alphabet = 'abcdefghijklmnopqrstuvwxyz'

def enbase(x):
    n = len(alphabet)
    if x < n:
        return alphabet[x]
    return enbase(x/n) + alphabet[x%n]

def debase(x):
    n = len(alphabet)
    result = 0
    for i, c in enumerate(reversed(x)):
        result += alphabet.index(c) * (n**i)
    return result

def average(a, b):
    a = debase(a)
    b = debase(b)
    return enbase((a + b) / 2)

print average('a', 'z') #m
print average('aa', 'zz') #mz
print average('cat', 'doggie') #budeel
print average('google', 'microsoft') #gebmbqkil
print average('microsoft', 'google') #gebmbqkil

Edit: Based on comments and other answers, you might want to handle strings of different lengths by appending the first letter of the alphabet to the shorter word until they're the same length. This will result in the "average" falling between the two inputs in a lexicographical sort. Code changes and new outputs below.

def pad(x, n):
    p = alphabet[0] * (n - len(x)) 
    return '%s%s' % (x, p)

def average(a, b):
    n = max(len(a), len(b))
    a = debase(pad(a, n))
    b = debase(pad(b, n))
    return enbase((a + b) / 2)

print average('a', 'z') #m
print average('aa', 'zz') #mz
print average('aa', 'az') #m (equivalent to ma)
print average('cat', 'doggie') #cumqec
print average('google', 'microsoft') #jlilzyhcw
print average('microsoft', 'google') #jlilzyhcw


If you mean the alphabetically, simply use FogleBird's algorithm but reverse the parameters and the result!

>>> print average('cat'[::-1], 'doggie'[::-1])[::-1]
cumdec

or rewriting average like so

>>> def average(a, b):
...     a = debase(a[::-1])
...     b = debase(b[::-1])
...     return enbase((a + b) / 2)[::-1]
... 
>>> print average('cat', 'doggie')
cumdec
>>> print average('google', 'microsoft') 
jlvymlupj
>>> print average('microsoft', 'google') 
jlvymlupj


It sounds like what you want, is to treat alphabetical characters as a base-26 value between 0 and 1. When you have strings of different length (an example in base 10), say 305 and 4202, your coming out with a midpoint of 3, since you're looking at the characters one at a time. Instead, treat them as a floating point mantissa: 0.305 and 0.4202. From that, it's easy to come up with a midpoint of .3626 (you can round if you'd like).

Do the same with base 26 (a=0...z=25, ba=26, bb=27, etc.) to do the calculations for letters:

cat becomes 'a.cat' and doggie becomes 'a.doggie', doing the math gives cat a decimal value of 0.078004096, doggie a value of 0.136390697, with an average of 0.107197397 which in base 26 is roughly "cumcqo"


Based on your proposed usage, consistent hashing ( http://en.wikipedia.org/wiki/Consistent_hashing ) seems to make more sense.


Thanks for everyone who answered, but I ended up writing my own solution because the others weren't exactly what I needed. I am trying to average app engine key names, and after studying them a bit more I discovered they actually allow any 7-bit ASCII characters in the names. Additionally I couldn't really rely on the solutions that converted the key names first to floating point, because I suspected floating point accuracy just isn't enough.

To take an average, first you add two numbers together and then divide by two. These are both such simple operations that I decided to just make functions to add and divide base 128 numbers represented as lists. This solution hasn't been used in my system yet so I might still find some bugs in it. Also it could probably be a lot shorter, but this is just something I needed to get done instead of trying to make it perfect.

# Given two lists representing a number with one digit left to decimal point and the
# rest after it, for example 1.555 = [1,5,5,5] and 0.235 = [0,2,3,5], returns a similar
# list representing those two numbers added together.
#
def ladd(a, b, base=128):
        i = max(len(a), len(b))
        lsum = [0] * i  
        while i > 1:
                i -= 1
                av = bv = 0
                if i < len(a): av = a[i]
                if i < len(b): bv = b[i]
                lsum[i] += av + bv
                if lsum[i] >= base:
                        lsum[i] -= base
                        lsum[i-1] += 1
        return lsum

# Given a list of digits after the decimal point, returns a new list of digits
# representing that number divided by two.
#
def ldiv2(vals, base=128):
        vs = vals[:]
        vs.append(0)
        i = len(vs)
        while i > 0:
                i -= 1
                if (vs[i] % 2) == 1:
                        vs[i] -= 1
                        vs[i+1] += base / 2
                vs[i] = vs[i] / 2
        if vs[-1] == 0: vs = vs[0:-1]
        return vs

# Given two app engine key names, returns the key name that comes between them.
#
def average(a_kn, b_kn):
        m = lambda x:ord(x)
        a = [0] + map(m, a_kn)
        b = [0] + map(m, b_kn)
        avg = ldiv2(ladd(a, b))
        return "".join(map(lambda x:chr(x), avg[1:]))

print average('a', 'z') # m@
print average('aa', 'zz') # n-@
print average('aa', 'az') # am@
print average('cat', 'doggie') # d(mstr@
print average('google', 'microsoft') # jlim.,7s:
print average('microsoft', 'google') # jlim.,7s:


import math
def avg(str1,str2):
    y = ''
    s = 'abcdefghijklmnopqrstuvwxyz'
    for i in range(len(str1)):
        x = s.index(str2[i])+s.index(str1[i])
        x = math.floor(x/2)
        y += s[x]
    return y

print(avg('z','a')) # m
print(avg('aa','az')) # am
print(avg('cat','dog')) # chm

Still working on strings with different lengths... any ideas?


This version thinks 'abc' is a fraction like 0.abc. In this approach space is zero and a valid input/output.

MAX_ITER = 10
letters = " abcdefghijklmnopqrstuvwxyz"
def to_double(name):
    d = 0
    for i, ch in enumerate(name):
        idx = letters.index(ch)
        d += idx * len(letters) ** (-i - 1)
    return d

def from_double(d):
    name = ""
    for i in range(MAX_ITER):
        d *= len(letters)
        name += letters[int(d)]
        d -= int(d)
    return name

def avg(w1, w2):
    w1 = to_double(w1)
    w2 = to_double(w2)
    return from_double((w1 + w2) * 0.5)

print avg('a', 'a') # 'a'
print avg('a', 'aa') # 'a mmmmmmmm'
print avg('aa', 'aa') # 'a zzzzzzzz'
print avg('car', 'duck') # 'cxxemmmmmm'

Unfortunately, the naïve algorithm is not able to detect the periodic 'z's, this would be something like 0.99999 in decimal; therefore 'a zzzzzzzz' is actually 'aa' (the space before the 'z' periodicity must be increased by one.

In order to normalise this, you can use the following function

def remove_z_period(name):
    if len(name) != MAX_ITER:
        return name
    if name[-1] != 'z':
        return name
    n = ""
    overflow = True
    for ch in reversed(name):
        if overflow:
            if ch == 'z':
                ch = ' '
            else:
                ch=letters[(letters.index(ch)+1)]
                overflow = False
        n = ch + n
    return n

print remove_z_period('a zzzzzzzz') # 'aa'


I haven't programmed in python in a while and this seemed interesting enough to try. Bear with my recursive programming. Too many functional languages look like python.

def stravg_half(a, ln):
     # If you have a problem it will probably be in here.
     # The floor of the character's value is 0, but you may want something different
     f = 0
     #f = ord('a')
     L = ln - 1
     if 0 == L:
          return ''
     A = ord(a[0])
     return chr(A/2) + stravg_half( a[1:], L)

def stravg_helper(a, b, ln, x):
    L = ln - 1
    A = ord(a[0])
    B = ord(b[0])
    D = (A + B)/2
    if 0 == L:
        if 0 == x:
             return chr(D)
        # NOTE: The caller of helper makes sure that len(a)>=len(b)
        return chr(D) + stravg_half(a[1:], x)
    return chr(D) + stravg_helper(a[1:], b[1:], L, x)

def stravg(a, b):
    la = len(a)
    lb = len(b)
    if 0 == la:
        if 0 == lb:
            return a # which is empty
        return stravg_half(b, lb)
    if 0 == lb:
        return stravg_half(a, la)
    x = la - lb
    if x > 0:
        return stravg_helper(a, b, lb, x)
    return stravg_helper(b, a, la, -x) # Note the order of the args
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜